This question already has answers here:
Closed 12 years ago.
Possible Duplicate:
How can I handle a keyboard shortcut when my program isn't active?
hello guys
i need to make a delphi program that creates a short cut key that works outside the delphi app. for example: when i press ctrl+1 it pastes a certain text. When i press ctrl+2, another text, and so on. It really helps my work. I managed to make a delphi application that does that, but it only works inside that app. i want it to work in all windows applications as long as my app is open ( and minimized). Can anyone help me out? I'm pretty new at delphi, i'm trying to learn.
I tried this code which someone recommended to me but it doesn't work. it does nothing. what did i do wrong?
unit Unit3;
interface
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, Clipbrd;
type
TForm17 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
HotKey1 : Integer;
HotKey2 : Integer;
procedure WMHotKey(var Msg: TWMHotKey); message WM_HOTKEY;
public
{ Public declarations }
end;
var
Form17: TForm17;
implementation
{$R *.dfm}
{ TForm17 }
procedure TForm17.FormCreate(Sender: TObject);
const
MOD_CONTROL = $0002;//0x0002
begin
// Register Ctrl + 1 hotkey
HotKey1 := GlobalAddAtom('Hotkey1');
RegisterHotKey(Handle, HotKey1, MOD_CONTROL, Ord('1'));
// Register Ctrl + 2 hotkey
HotKey2 := GlobalAddAtom('Hotkey2');
RegisterHotKey(Handle, HotKey2, MOD_CONTROL, Ord('2'));
end;
procedure TForm17.FormDestroy(Sender: TObject);
begin
//unregister the hotkeys
UnRegisterHotKey(Handle, HotKey1);
GlobalDeleteAtom(HotKey1);
UnRegisterHotKey(Handle, HotKey2);
GlobalDeleteAtom(HotKey2);
end;
procedure TForm17.WMHotKey(var Msg: TWMHotKey);
begin
if Msg.HotKey = HotKey1 then
begin
ShowMessage('Ctrl + 1 was pressed');
Clipboard.AsText := 'This is my own text!';
end
else
if Msg.HotKey = HotKey2 then
begin
ShowMessage('Ctrl + 2 was pressed');
Clipboard.AsText := 'This is my own text!';
end;
end;
end.
You need to use RegisterHotKey and UnregisterHotKey from the Win32 API, they are very straightforward to use.
Also, you may find useful ShortCutToKey(), which returns the key code and shift state of a Delphi shortcut.
PS: Don't forget to check the return value of RegisterHotKey(), since it will fail if the hotkey is already registered by other application.
Edit: sorry, I though that you were using another WM_MESSAGE, since first you posted the code as plain text and I only scanned through it...
I think that the problem with your code is that your are using GlobalAddAtom for the ID key, but you only need to use an unique ID inside your app (the docs for the function say that you need to use GlobalAddAtom only for a shared DLL). Try using just this:
const
ID_HOTKEY1=0;
ID_HOTKEY2=1;
procedure TYourForm.FormCreate(Sender: TObject);
begin
if not RegisterHotKey(Handle,ID_HOTKEY1,MOD_CONTROL,Ord('1'))
then Application.MessageBox('Error registering hot key 1','Error',MB_ICONERROR);
if not RegisterHotKey(Handle,ID_HOTKEY2,MOD_CONTROL,Ord('2'))
then Application.MessageBox('Error registering hot key 2','Error',MB_ICONERROR);
end;
procedure TYourForm.FormDestroy(Sender: TObject);
begin
UnregisterHotKey(Handle,ID_HOTKEY1);
UnregisterHotKey(Handle,ID_HOTKEY2);
end;
procedure TYourForm.WMHotKey(var Msg: TWMHotKey);
begin
Application.MessageBox(PChar(IntToStr(Msg.HotKey)),'Hotkey ID',MB_OK);
end;
Also, the MOD_CONTROL and related constants are already defined by Delphi, you don't need to redefine them.
andrei, check this sample code to paste a text in a external application using a hotkey.
the code show two options
1) sending the Ctrl+V combination to the focused window
2) sending a WM_PASTE message
function GetFocusedHandle: THandle;
var
ActiveHWND : THandle;
FocusedThread : DWORD;
begin
Result:=0;
ActiveHWND := GetForegroundWindow;
FocusedThread := GetWindowThreadProcessID(ActiveHWND, nil) ;
try
if AttachThreadInput(GetCurrentThreadID, FocusedThread, true) then
Result := GetFocus;
finally
AttachThreadInput(GetCurrentThreadID, FocusedThread, false) ;
end;
end;
procedure TForm17.WMHotKey(var Msg: TWMHotKey);
var
FocusWindowHwnd : THandle;
begin
if Msg.HotKey = HotKey1 then //option 1
begin
Clipboard.AsText := 'Text from Ctrl + 1 Hotkey';//Assign the text to the clipboard
//send the Ctrl + V combination to the current focused window
keybd_event(VK_CONTROL, 0, 0, 0);
keybd_event(Ord('V'), 0, 0, 0);
end
else
if Msg.HotKey = HotKey2 then //option 2
begin
FocusWindowHwnd:=GetFocusedHandle; //get the handle to the focused window
if FocusWindowHwnd<>0 then
begin
Clipboard.AsText := 'Text from Ctrl + 2 Hotkey';//Assign the text to the clipboard
SendMessage(FocusWindowHwnd,WM_PASTE,0,0);//send the WM_PASTE message
end;
end;
end;
Related
Using the code below, or maybe modifying it, possible to achive my goal?
Or not by using this code, but it must be joystick buttons using when form is hidden in tray.
Thanks
type
TForm125 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
HotKey1 : Integer;
procedure WMHotKey(var Msg: TWMHotKey); message WM_HOTKEY;
public
end;
var
Form125: TForm125;
implementation
{$R *.dfm}
procedure TForm125.FormCreate(Sender: TObject);
begin
HotKey1 := GlobalAddAtom('MyAppHotkey1');//create a unique value for identify the hotkey
if not RegisterHotKey(Handle, HotKey1, MOD_CONTROL, VK_F1) then //register the hotkey CTRL + F1
ShowMessage('Sorry can not register the hotkey');
end;
procedure TForm125.FormDestroy(Sender: TObject);
begin
UnRegisterHotKey(Handle, HotKey1);//unregister the hotkey
GlobalDeleteAtom(HotKey1);//remove the atom
end;
procedure TForm125.WMHotKey(var Msg: TWMHotKey);
begin
if Msg.HotKey = HotKey1 then
ShowMessage('Hello'); // do your stuff
end;
Sorry, this is a follow up on Chris' answer, but it seems OP needs a little more assistance.
I also believe that the use of a joystick component is the way to go.
For example, NLDJoystick. The installation instructions are included, as well as a mini manual.
You will need to follow these steps:
Place the component on your form,
Set Active to True (this won't succeed when there is no joystick attached),
Implement the OnButtonDown event, as follows:
procedure TForm1.NLDJoystick1ButtonDown(Sender: TNLDJoystick;
const Buttons: TJoyButtons);
begin
Beep;
end;
The TJoyButtons type is a set of JoyBtn1..JoyBtn32, so if you wish you can react to a specific button, or a combination of multiple pressed buttons:
procedure TForm1.NLDJoystick1ButtonDown(Sender: TNLDJoystick;
const Buttons: TJoyButtons);
begin
if JoyBtn1 in Buttons then Beep;
//or:
if Buttons = [JoyBtn1, JoyBtn2] then Beep;
end;
Note that if Advanced is False (the default setting) that there are only 4 buttons supported.
You can check the state of the buttons of your joystick(s) when you need to check them... if works even if the form is hidden:
uses ..., MMSystem;
const
iJoystick = 1; // ID of the joystick
var
myjoy : TJoyInfoEx;
begin
myjoy.dwSize := SizeOf(myjoy);
myjoy.dwFlags := JOY_RETURNALL;
if (joyGetPosEx(iJoystick, #myjoy) = JOYERR_NOERROR) then
begin
if (myjoy.wbuttons and joy_button1) > 0 then // you can do it for all the buttons you need
begin
ShowMessage('button 1 down');
end;
end;
end;
Eventually, you can create a timer which often checks their status to know if the status has change and trigger what you need...
I have ID of the process. This process is an application which have a main window.
I am trying to close this application by sending WM_CLOSE to its main window.
I am searching its main window by using EnumWindows.
The problem is, that this application which I try to close, does not close always.
It is multithreaded application. Notepad and Calc are always closing when I use the same method which is presented below. But I am not sure if it is working properly cause it returns me many handles to the same window, even for Calc.exe.
Is it possible that thread is taking a handle to window and then this handle somehow become corrupted? Or maybe I should not use GetWindowThreadProcessId(hHwnd,pPid) but some other function in the callback?
I am out of ideas, would be grateful for any help. Thanks.
Code snippet:
unit Unit22;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm22 = class(TForm)
edtprocID: TEdit;
lblEnterProcessID: TLabel;
btnCloseProcessWindow: TButton;
lblStatus: TLabel;
procedure btnCloseProcessWindowClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
type
THandleAndHWND = record
ProcID: THandle;
WindowHandle: HWND;
end;
var
Form22: TForm22;
var
HandleAndHWNDArray: array of THandleAndHWND;
HandeIndex, lp: Integer;
implementation
{$R *.dfm}
function EnumProcess(hHwnd: HWND; lParam : integer): boolean; stdcall;
var
pPid : DWORD;
begin
//if the returned value in null the
//callback has failed, so set to false and exit.
if (hHwnd=0) then
begin
result := false;
end else
begin
GetWindowThreadProcessId(hHwnd,pPid);
Inc(HandeIndex);
HandleAndHWNDArray[HandeIndex].ProcID := pPid;
HandleAndHWNDArray[HandeIndex].WindowHandle := hHwnd;
Result := true;
end;
end;
procedure TForm22.btnCloseProcessWindowClick(Sender: TObject);
var
ProcID: Cardinal;
i, LastError: Integer;
begin
HandeIndex := -1;
ProcID := StrToInt(edtprocID.Text);
SetLength(HandleAndHWNDArray, 3000);
EnumWindows(#EnumProcess,lp);
for i := 0 to HandeIndex do //After EnumWindows HandleIndex is above 500 despite the fact that I have like 10 openned windows max
begin //That means that EnumWindows was called 500 times?
if HandleAndHWNDArray[i].ProcID = ProcID then //search for process equal to procces ID given by the user
begin
//if we have a processID then we have a handle to its main window
if PostMessage(HandleAndHWNDArray[i].WindowHandle, WM_CLOSE, 0, 0) then
begin
lblStatus.Caption := 'message posted!';
end else
begin
LastError := GetLastError;
lblStatus.Caption := Format('Error: [%d] ' + SysErrorMessage(LastError), [LastError]);
end;
Exit;
end;
end;
end;
end.
Have a look in this Knowledge Base Article here on how to close another application as cleanly as possible. You are doing it right so far. The Article suggests that you
first post WM_CLOSE to all windows of the application (since you cannot know for sure which one is the main).
wait with a timeout and if the timeout elapses
kill the application using TerminateProcess
I agree.
Depending on the chosen filter, I'd like the OpenDialog to 'look' in different directries.
Something like:
procedure TForm1.FileOpen1OpenDialogTypeChange(Sender: TObject);
// This does not work as intended...
var
Dialog: TOpenDialog;
FilterIndex: Integer;
FilterExt: string;
Path: string;
begin { TForm1.actFileOpenOpenDialogTypeChange }
Dialog := Sender as TOpenDialog;
FilterIndex := Dialog.FilterIndex;
FilterExt := ExtFromFilter(Dialog.Filter, FilterIndex);
GetIniPathForExtension(FilterExt, Path);
if DirectoryExists(Path) and
(Path <> IncludeTrailingPathDelimiter(Dialog.InitialDir)) then
begin
// those two statements don't have the desired effect
// but illustrate what is meant to happen:
Dialog.FileName := Path + '*' + FilterExt;
Dialog.InitialDir := Path;
end;
end; { TForm1.actFileOpenOpenDialogTypeChange }
I can't find any way to let the dialog update itself to the new directory.
I've tried calling OpenDialog.Execute, but that starts another OpenDialog without closing the current one...
Some time ago I have looked after exactly that sort of thing, but couldn't find a solution either. Nowadays I'm glad not to implement it anyway for the following reason:
Imagine a user executes the open dialog. He knows where to find the required file and navigates to that folder. Now he can't see the file and realizes that the filter is set wrong. He changes the filter and naturally expects the folder to stay the same.
Try and make some observations: in most of the cases a user first selects the folder and after that the file type.
While the below is not exactly elegant, tested with 2K, XP, Vista and 7, it seems to work. The idea is to use the dialog's behavior that, when a valid directory is entered into the file name box, if 'Open' button is pressed, the dialog switches to that folder.
It does not work with 'Vista style' dialogs, I don't have any acquaintance with the Common Item Dialog. So the UseLatestCommonDialogs must be set to false before showing a dialog. Also note that the OnTypeChange event is not fired when the dialog is initially launched, one can set the FilterIndex and InitialDir before showing the dialog.
type
TForm1 = class(TForm)
Button1: TButton;
OpenDialog1: TOpenDialog;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure OpenDialog1TypeChange(Sender: TObject);
procedure OpenDialog1FolderChange(Sender: TObject);
private
FDlgCleanUp: Boolean;
FDlgFocusCtrl: HWnd;
FSaveDlgFName: array [0..255] of Char;
public
end;
[...]
uses
CommDlg, Dlgs;
procedure TForm1.Button1Click(Sender: TObject);
begin
if OpenDialog1.Execute then
ShowMessage(OpenDialog1.FileName);
end;
type
TFileExt = (feText = 1, feRichText, feDocument);
const
FileExts: array [TFileExt] of string = ('txt', 'rtf', 'doc');
FileExtDesc: array [TFileExt] of string =
('text (*.txt)', 'rich text (*.rtf)', 'document (*.doc)');
procedure TForm1.FormCreate(Sender: TObject);
var
fe: TFileExt;
begin
OpenDialog1.Options := OpenDialog1.Options - [ofOldStyleDialog];
NewStyleControls := True;
UseLatestCommonDialogs := False;
OpenDialog1.Filter := '';
for fe := Low(FileExts) to High(FileExts) do
OpenDialog1.Filter := OpenDialog1.Filter +
FileExtDesc[fe] + '|*.' + FileExts[fe] + '|';
end;
function GetIniPathForExtension(const Ext: string): string;
begin
// Get corresponding path from an ini file....
Result := ExtractFilePath(Application.ExeName) + Ext;
end;
procedure TForm1.OpenDialog1TypeChange(Sender: TObject);
var
Dialog: TOpenDialog;
Dlg: HWnd;
Path: string;
begin
Dialog := Sender as TOpenDialog;
Dlg := GetParent(Dialog.Handle);
Path := GetIniPathForExtension(FileExts[TFileExt(Dialog.FilterIndex)]);
ForceDirectories(Path);
// remember what's in file name, have to put it back later
GetDlgItemText(Dlg, cmb13, #FSaveDlgFName, 256);
SendMessage(GetDlgItem(Dlg, cmb13), WM_SETREDRAW, 0, 0); // reduce flicker
FDlgFocusCtrl := GetFocus;
// set file name to new folder
SendMessage(Dlg, CDM_SETCONTROLTEXT, cmb13, Longint(PChar(Path)));
// weird OS: windows - the below is only necessary for XP. 2K, Vista and 7
// clicks fine without it, XP does not!
windows.SetFocus(GetDlgItem(Dlg, IDOK));
// do not cleanup here, with Vista and 7 folder change seems to happen
// asynchronously - it might occur later than setting the file name and that
// clears/reverts the edit box.
FDlgCleanUp := True;
// click 'Open' to change to folder
SendMessage(GetDlgItem(Dlg, IDOK), BM_CLICK, IDOK, 0);
end;
procedure TForm1.OpenDialog1FolderChange(Sender: TObject);
var
Dlg: HWnd;
begin
// set the file name and focus back
if FDlgCleanup then begin // do not intervene if we didn't cause the change
Dlg := GetParent((Sender as TOpenDialog).Handle);
SendMessage(GetDlgItem(Dlg, cmb13), WM_SETREDRAW, 1, 0);
SetDlgItemText(Dlg, cmb13, #FSaveDlgFName);
windows.SetFocus(FDlgFocusCtrl);
end;
FDlgCleanup := False;
end;
One possibility:
var
ShowAfterClose: boolean = false;
MemFilterIndex: integer;
procedure TForm1.Import1Click(Sender: TObject);
begin
//...
with OpenDialogImport do
repeat
if Execute then
begin
ReadImportedFile(FileName); //Do action
exit;
end else begin
if not ShowAfterClose then //Check ShowAfterClose
exit;
ShowAfterClose := false; //Set ShowAfterClose false
FilterIndex := MemFilterIndex; //Copy MemFilterIndex
end;
until false;
//...
end;
procedure TForm1.OpenDialogImportTypeChange(Sender: TObject);
begin
PostMessage(TOpenDialog(Sender).handle,
WM_KEYDOWN, VK_ESCAPE , 0); //Cancel dialog
TOpenDialog(Sender).InitialDir := 'C:\'; //Set new directory
MemFilterIndex := TOpenDialog(Sender).FilterIndex; //Remember filter index
ShowAfterClose := True; //ShowAfterClose = True
end;
I'll agree with everyone else to date... it's VERY BAD user interface design to change things without asking the user, and/or against the user's wishes.
I have a delphi app that runs minimized to a tray icon. When the tray icon is double clicked the app opens a non-modal user interface form.
I have added logic to the app to detect whether it is already running. If it isn't running, it starts up and miminizes itself to the tray.
If it is already running, I want it to pass control to the first instance of itself and open the non-modal form, and then exit (the second instance). What's the best way to do this?
TIA
R
The recommended method of detecting another instance of a given application is for that application to create a named mutex or lock a file in a well known location, so that the second instance will trigger an error when you try to create the same mutex or lock the same file. Once you know there's another instance running, you can find the process handle for that instance and send it a message to restore if its minimized.
Microsoft way is not flawless, so i do prefer old school:
const WM_KNOCK_KNOCK = WM_USER + 42;
{ or WM_USER + 265 or any number you like, consult PSDK documentation why WM_USER range }
{ or do RegisterWindowMessage }
{...}
procedure TMainForm.FormCreate(Sender: TObject);
var
Window: HWND;
begin
Window := FindWindow(PChar({MainForm.}ClassName), nil);
{
i neither remember how it works exactly nor have time to investigate right now,
so quick and dirty validity test follows:
}
Assert(not (HandleAllocated and (Window = Handle)), 'failed, use fallback');
{
if Window <> 0 then
begin
PostMessage(Window, WM_KNOCK_KNOCK, 0, 0);
Halt;
end;
{ regular initialization }
end;
Now, WM_KNOCK_KNOCK message handler of first instance performs wakeup routine.
i have little clue what exactly you do when you receive WM_LBUTTONUP (or perhaps WM_LBUTTONDBLCLK) in your Shell_NotifyIcon wrapper (Application.Restore, maybe?). As, Chris Thornton said, there is no such state as 'minimized to tray', it is artifical.
Fallback: if assertion fails, note what code depends only on class function ClassName so could be easily moved out of FormCreate and invoked before Application creates it.
program Only_One_Mutex;
//undefine this {.$define useMutex} to make it a multi instance app.
{$define useMutex}
uses
Forms,
Windows,
Messages,
MainForm in 'MainForm.pas' {frmMain};
{$R *.res}
{$ifdef useMutex}
var
Mutex : THandle;
{$endif}
function pBuffStr( Var S1: String; S:String ): PChar;
begin
FillChar(S1,SizeOf(S1),#0); {clear out the destination string}
S1:= S+#0; {set it equal the source}
Result:= #S1[1]; {result is a PChar pointer }
end;
procedure WindowToTop( WN: String );
var
iTitle: integer;
S1,S : String;
Done: Boolean;
begin
Done:= False;
While NOT Done do begin
if Pos(';',WN) > 0 then begin
S:= Copy(WN,1,Pos(';',WN)-1);
WN:= Copy(WN,Pos(';',WN)+1,Length(WN));
end else begin
S:= WN;
Done:= True;
end; {if Pos}
iTitle:= FindWindow( nil, pBuffStr(S1,S) );
if iTitle <> 0 then
if NOT SetForegroundWindow( iTitle ) then
GetLastError();
Application.ProcessMessages;
end; {while NOT Done}
end;
procedure RestoreWindow( WN: String );
var
iTitle: integer;
Dest, S : String;
Done: Boolean;
begin
Done:= False;
While NOT Done do begin
if Pos(';',WN) > 0 then begin {is there more than ONE name}
S:= Copy(WN,1,Pos(';',WN)-1); {copy the first name of the original}
WN:= Copy(WN,Pos(';',WN)+1,Length(WN)); {reduce the original string}
end else begin
S:= WN; {only one name, so copy it}
Done:= True; {this loop is done}
end; {if Pos}
iTitle:= FindWindow( nil, pBuffStr(Dest,S) ); {search for the window name}
if iTitle <> 0 then {if found, then restore it}
DefWindowProc(iTitle, WM_SYSCOMMAND, SC_RESTORE, SC_RESTORE);
end; {while NOT Done}
end;
//=================================================================
procedure AppRun;
begin
Application.Initialize;
Application.Title := 'Only One Prog';
Application.CreateForm(TfrmMain, frmMain);
Application.Run;
end;
begin
{$ifdef useMutex}
//global var declarations in the mainform.
{=====================================================================}
//ATitle MUST match the assigned Application.Title in AppRun
//and Application.Title can "NOT" be a constant or var.
ATitle := 'Only One Prog';
{ THIS IS HOW IT KEEPS THE SECOND INSTANCE FROM STARTING,
by using a MUTEX, and a MAINFORM window title }
//any text appender will work.
AMutex := ATitle + ' Mutex Thu, Jul/12/2012';
//mainform's caption
ACaption := ATitle + ', Mainform Caption';
//a label on the mainform
ALabel := ATitle + ', MainForm Label-using mutex';
{=====================================================================}
Mutex := CreateMutex(nil, True, PAnsiChar( AMutex ));
if (GetLastError = ERROR_ALREADY_EXISTS) then begin
try
RestoreWindow( ACaption );
WindowToTop( ACaption ); //main form's name
finally
CloseHandle(Mutex);
end;
end else
if (Mutex <> 0)
AND (GetLastError <> ERROR_ALREADY_EXISTS)
then begin
try
AppRun;
finally
CloseHandle(Mutex);
end;
end;
{$else}
//global var declarations in the mainform.
{=====================================================================}
ATitle := 'More than One'; //global declaration in the mainform.
//mainform's caption - THIS IS HOW IT KEEPS THE SECOND INSTANCE FROM STARTING
ACaption := ATitle + ', Mainform Caption';//global declaration in the mainform.
//a label on the mainform
ALabel := ATitle + ', MainForm Label-multi exe'; //global declaration in the mainform.
{=====================================================================}
AppRun;
{$endif}
end.
unit MainForm;
interface
uses
Windows, Messages, SysUtils,
Variants, Classes, Graphics,
Controls, Forms, Dialogs, StdCtrls, LblEffct;
type
TfrmMain = class(TForm)
le1: TLabelEffect;
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
frmMain: TfrmMain;
//these GLOBAL vars, are assigned values in the program source (.dpr) file.
ATitle,
ACaption,
ALabel,
AMutex :String;
implementation
{$R *.dfm}
procedure TfrmMain.FormCreate(Sender: TObject);
begin
Caption := ACaption; //used to ID this form...
le1.Caption := ALabel;
end;
end.
While showing a save dialog, I want to hook user's filter type change and change file extension automatically. (e.g. like MSPaint's "Save As" operation.)
With TSaveDialog and setting UseLatestCommonDialogs := False,
I can handle this by the following code. (without latest common dialog support, of cource.)
procedure TForm1.SaveDialog1TypeChange(Sender: TObject);
var
FName, Ext: string;
begin
with TSaveDialog(Sender) do
begin
if DirectoryExists(FileName) then // FileName is Empty
exit;
case FilterIndex of
1: Ext := '.png';
2: Ext := '.bmp';
3: Ext := '.jpg';
end;
FName := ChangeFileExt(ExtractFileName(FileName), Ext);
SendMessage(Windows.GetParent(Handle), CDM_SETCONTROLTEXT, 1152, LongInt(PChar(FName)));
end;
end;
I want to support both XP, and vista/7 with Delphi 2007.
Should I use TFileSaveDialog instead of TSaveDialog with internal wrapper ?
(And I have to struggle with COM programming using IFileDialogControlEvents ?)
Or can I achieve this with TFileSaveDialog and it's standard properties only ?
(My development environment is still on XP machine, so I've never tried. sorry.)
I think it's very common task, but I couldn't find any sample code supporting Vista/7...
As far as I know, TFileSaveDialog will raise an exception on XP. It needs Vista or up.
Update: some D2010 code for TFileSaveDialog adapted from your event handler....
(I don't have D2007 on Vista; use PWideChar instead of PChar)
procedure TForm1.FileSaveDialog1TypeChange(Sender: TObject);
var
FName, Ext: string;
pName: PChar;
begin
with TFileSaveDialog(Sender) do
begin
if DirectoryExists(FileName) then // FileName is Empty
exit;
case FileTypeIndex of
1: Ext := '.png';
2: Ext := '.bmp';
3: Ext := '.jpg';
end;
Dialog.GetFileName(pName);
FName := ChangeFileExt(ExtractFileName(pName), Ext);
Dialog.SetFileName(PChar(FName));
end;
end;
Where the FileSaveDialog is:
object FileSaveDialog1: TFileSaveDialog
FavoriteLinks = <>
FileTypes = <
item
DisplayName = 'png files'
FileMask = '*.png'
end
item
DisplayName = 'bmp files'
FileMask = '*.bmp'
end
item
DisplayName = 'jpg files'
FileMask = '*.jpg'
end>
Options = []
OnTypeChange = FileSaveDialog1TypeChange
end
You wrote that you couldn't hack the wrapper. I use this code for my XLSX/XLS/ODS exporting library to change the file extension on both XP and Vista+.
One drawback: Class helpers cannot access private fields in Delphi 2007, so this code works only in Delphi 2009+. If you want Delphi 2007 compatibility, use the same hack for TOpenDialog like I used for TFileDialogWrapper in this example.
{ interface }
//some hacking needed to change the file extension at type change,
//empty class is just fine...
TFileDialogWrapper = class(TObject)
private
{$HINTS OFF}
procedure AssignFileTypes;
procedure AssignOptions;
function GetFileName: TFileName;
function GetHandle: HWND;
procedure HandleShareViolation(Sender: TObject;
var Response: TFileDialogShareViolationResponse);
procedure OnFileOkEvent(Sender: TObject; var CanClose: Boolean);
procedure OnFolderChangeEvent(Sender: TObject);
procedure OnSelectionChangeEvent(Sender: TObject);
procedure OnTypeChangeEvent(Sender: TObject);
protected
FFileDialog: TCustomFileDialog;
{$HINTS ON}
end;
TOpenDialogHelper = class helper for TOpenDialog
public
function GetInternalWrapper: TFileDialogWrapper;
end;
{ implementation }
{ TOpenDialogHelper }
function TOpenDialogHelper.GetInternalWrapper: TFileDialogWrapper;
begin
Result := TFileDialogWrapper(Self.FInternalWrapper);
end;
{ TFileDialogWrapper }
procedure TFileDialogWrapper.AssignFileTypes;
begin
end;
procedure TFileDialogWrapper.AssignOptions;
begin
end;
function TFileDialogWrapper.GetFileName: TFileName;
begin
end;
function TFileDialogWrapper.GetHandle: HWND;
begin
end;
procedure TFileDialogWrapper.HandleShareViolation(Sender: TObject;
var Response: TFileDialogShareViolationResponse);
begin
end;
procedure TFileDialogWrapper.OnFileOkEvent(Sender: TObject;
var CanClose: Boolean);
begin
end;
procedure TFileDialogWrapper.OnFolderChangeEvent(Sender: TObject);
begin
end;
procedure TFileDialogWrapper.OnSelectionChangeEvent(Sender: TObject);
begin
end;
procedure TFileDialogWrapper.OnTypeChangeEvent(Sender: TObject);
begin
end;
//use this for OnTypeChane event of a "normal" TOpenDialog / TSaveDialog
procedure TForm1.DialogTypeChange(Sender: TObject);
var
xFN: WideString;
xExporter: TOCustomExporter;
xFileName: PWideChar;
xFD: TFileDialogWrapper;
xFilterIndex: UINT;
begin
if Sender is TOpenDialog then
with TOpenDialog(Sender) do begin
xFD := GetInternalWrapper;
if (xFD <> nil) and (xFD.FFileDialog <> nil)
then begin
//Vista file dialog
xFD.FFileDialog.Dialog.GetFileName(xFileName);
if xFileName = '' then
exit;
xFN := xFileName;
xFD.FFileDialog.Dialog.GetFileTypeIndex(xFilterIndex);
// DO WHATEVER YOU WANT WITH THE FILENAME HERE //
xFD.FFileDialog.Dialog.SetFileName(PWideChar(xFN));
end else begin
//Old dialog
xFN := ExtractFileName(FileName);
if xFN = '' then
exit;
// DO WHATEVER YOU WANT WITH THE FILENAME HERE //
{$HINTS OFF}
SendMessage(Windows.GetParent(Handle), CDM_SETCONTROLTEXT, 1152, LongInt(PWideChar(xFN)));
{$HINTS ON}
end;
end;
end;
EDIT: actually, if you set the DefaultExt property, Delphi/Windows care about the file extension change for you. In that case you don't have to do anything in the OnTypeChange event.
This feature is implemented in Delphi, but disabled by default.
In order to activate it, just entry the default extension in DefaultExt property.