Window Subclassing alternative (2)
You probably be familiar with the usage of common dialogs (open / save files, select font / color, and finding and replacement), but do you learn how to call the Select Folder dialog? If the answer is, you can take a simple example, you will be a warm body. If you think that you have already understood it, you can skip the following.
To call the "Select Folder" dialog, and the methods used by other general dialogs are very similar: BrowseInfo adds a function (shbrowseforfolder). Please see the code:
Procedure TFORM1.BUTTON2CLICK (Sender: TOBJECT);
VAR
BI: Browseinfo;
Szdisplay: array [0..max_path] of char;
PIDL: PitemidList;
Str: string;
Begin
With Bi Do Begin
Hwndowner: = handle;
PIDLROOT: = NIL;
PszdisplayName: = szdisplay;
Lpsztitle: = 'SELECT A DIRECTORY';
Ulflags: = Bif_Returnonlyfsdirs or bif_statustext;
LPFN: = @BrowseCallback;
LPARAM: = 0;
END;
PIDL: = shbrowseforfolder (BI);
IF PIDL <> nil kilin
SETLENGTH (STR, MAX_PATH);
ShgetPathfromidList (PIDL, PCHAR (STR));
Str: = PCHAR (STR);
Caption: = STR;
CotaskMemFree (PIDL);
END;
END;
ShbrowseForfolder returns a LPITEMIDLIST, you need to manually convert it into an actual file path (unless you select a virtual path such as the recycle bin and the control panel). Finally, use the Shell API to release the obtained PIDL. In the above code, BrowseCallback is a callback function you have written. If you don't want to handle your callback, you can set it to nil. I still handled this function because I need some of its features, as follows:
Function Browsecallback (awnd: hwnd; umsg: uint; lp, lpdata: lparam): integer; stdcall;
VAR
StrPath: String;
PIDL: PitemidList;
Begin
Case UMSG of
Bffm_selchanged:
Begin
PIDL: = PitemidList (LP);
IF PIDL <> nil kilin
SetLength (StrPath, Max_Path);
SHGETPATHFROMIDLIST (PIDL, PCHAR (STRPATH));
StrPath: = PCHAR (STRPATH);
StrPath: = 'Folder SELECTED:' STRPATH;
SendMessage (awnd, bffm_setstatustext, 1, longint (pchar (strpath)));
END;
END;
END;
Result: = 0;
END;
The BrowseCallback function can accept some notification messages, such as Bffm_Selchanged, which is listed above, triggered when the user selects another item in the folder list, the programmer can use some other messages (such as bffm_setstatustext) update the other corresponding section. The introduction to ShbrowseForfolder said that so much is enough. However, I am not satisfied with such monotonous interfaces. One of the most direct ideas is: I hope to add a list in the dialog box, which lists some common folders for users to choose, without the need to once every time the "courtyard deep" hierarchy tree once again Click. This is another good place to use Subclass. Remember what I mentioned in one of the series of this article? To use Subclass technology, the full essential condition is to get a handle of a window. Very lucky, here we have a very simple way to get this handle, because the dialog is initialized after successfully sends the BFFM_Initialized notification to the callback function described above, and our subclass work is done here.
Add the following Message Dispatcher to the browsecallback function:
Case UMSG of
Bffm_initialized:
Begin
OldbrowseProc: = TwindowProc (getWindowlong (awnd, gwl_wndproc);
SetWindowlong (awnd, gwl_wndproc, longint (@newbrowseproc);
Adjustdlg (AWND);
END;
Where oldbrowseproc is a variable declared in the Implementation section:
VAR
OldbrowseProc: twindowproc = nil;
NEWBROWSEPROC and AdjustDLG are all written functions, they are all longer, I will tell their contents.
Let's see Adjustdlg's work first. Its task is to add a combo box to the dialog and add several items to it. It sounds very simple, but there are many trivial work must be done. Because we are subclass on the system-defined window, the VCL is basically helping here: we must use a lot of API.
Procedure Adjustdlg (AWND: HWND);
VAR
Wnd: hwnd;
Wndcombo: hwnd;
RC: TRECT;
Found: boolean;
Classname: array [0..80] of char;
SaveRect: TRECT;
OldStyle: Integer;
Begin
// Find The TreeView First
Wnd: = getWindow (awnd, gw_child);
Found: = false;
While Iswindow (WND) DO BEGIN
GetClassName (WND, ClassName, 80);
IF lstrcmpi (classname, 'systeeview32') = 0 THEN BEGIN
Found: = True;
Break;
END;
Wnd: = getWindow (WND, GW_HWNDNEXT);
END;
IF not found kilone
In order to make the inserted ComboBox and other window controls, you first need to find the TreeView window to display the folder. My plan is to let ComboBox occupy the original location of TreeView (of course its height is much smaller than TreeView), then, other windows including TreeView are moved down. Here is the implementation code: // Add Combo Box and Move Other Controls Down
GetWindowRect (WND, RC);
ScreenToClient (awnd, rc.topleft);
ScreenToClient (awnd, rc.bottomright);
Wndcombo: = CREATEWINDOW ('ComboBox', '',
WS_CHILD or WS_VISible or CBS_DropDownloadList or CBS_OWNERDRAWFIXED or CBS_HASSTRINGS,
Rc.Left, rc.top,
Rc.right-rc.left, rc.bottom-rc.top,
AWND, HMENU (IDC_COMBO),
Hinstance, NIL);
SendMessage (WNDCOMBO, WM_SETFONT,
SendMessage (AWnd, WM_GETFONT, 0, 0),
1);
OldStyle: = getWindowlong (WND, GWL_STYLE);
Setwindowlong (WND, GWL_STYLE, OLDSTYLE OR TVS_SHOWSELALWAYS);
SaveRect: = RC;
Wnd: = getWindow (awnd, gw_child);
While Iswindow (WND) DO BEGIN
GetWindowRect (WND, RC);
ScreenToClient (awnd, rc.topleft);
ScreenToClient (awnd, rc.bottomright);
IF (WND <> WNDCOMBO) and (rc.top> = saverect.top) THEN
SetWindowPOS (WND, HWND_NOTOPMOST, RC.LEFT, RC.TOP 40, 0, 0, SWP_NOSUE OR SWP_NOZORDER);
Wnd: = getWindow (WND, GW_HWNDNEXT);
END;
GetWindowRect (awnd, rc);
SetWindowPos (awnd, hwnd_notopmost, 0, 0, rc.right-rc.left, rc.bottom-rc.top 40, swp_nomove or swp_nozorder);
If you have used the API written program in the past, then these code may make you see a little dizzy. Basically the above procedures completed the following:
(1) Calculate the position of TreeView in the window;
(2) Establish a ComboBox window and place it to a reasonable place based on the location of TreeView;
(3) Set the font of ComboBox to the same font of the entire form (this step is necessary, otherwise the effect will be difficult to see);
(4) Add a TVS_SHOWSELALWAYS bit to the TreeView window, so that when the focus moves to ComboBox, it still observes which items in TreeView in TreeView;
(5) Switching other controls in the window, thus making the necessary space for ComboBOX; (6) the height of the window itself is also slightly large, thereby adapting the size of the addition of ComboBox.
The next step is to add some items to ComboBox, otherwise it is a chicken rib. I decided to add two items: (1) some special path in the system, which can be obtained by ShgetSpecialFolderLocation; (2) The usual file path. In order to make the code simple, I added a auxiliary function:
Procedure INSERTCOMBOITEM (HCOMBO: HWnd; const text: string; data: dword);
VAR
NINDEX: Integer;
Begin
NINDEX: = SendMessage (hcombo, cb_addstring, 0, longint (pchar (text)));
SendMessage (HCOMBO, CB_SETITEMDATA, NINDEX, DATA);
END;
Then add the following code at the end of the AdjustDLG function:
INSERTCOMBOITEM (WNDCOMBO, '', CSIDL_DESKTOP);
INSERTCOMBOITEM (WNDCOMBO, '', CSIDL_FAVORITES);
INSERTCOMBOITEM (WNDCOMBO, '', CSIDL_STARTMENU);
INSERTCOMBOITEM (WNDCOMBO, '', CSIDL_DRIVES);
INSERTCOMBOITEM (WNDCOMBO, 'C: /', 555);
INSERTCOMBOITEM (WNDCOMBO, 'D: / WINNT', 555);
INSERTCOMBOITEM (WNDCOMBO, 'C: / Windows / System', 555);
There is no special meaning with 555 here. I originally wanted to sign a normal folder with 0, but then CSIDL_DESKTOP is defined as 0, so you must use other numbers to distinguish. 555 is written by my belief, you can of course use other numbers, just pay attention to not conflict with predefined CSIDL constants.
The content of the AdjustDlg function is so much. Next is the content of the newbrowseproc function, its basic structure is as follows:
Function newbrowseproc (awnd: hwnd; umsg: uint; wp: wparam; lp: lparam): longint; stdcall;
Begin
Result: = 0;
Case UMSG of
...
END;
IF assigned (oldbrowseproc) THEN
Result: = OldBrowseProc (AWND, UMSG, WP, LP);
END;
Several messages must be processed in NewbrowSeproC. The first is that when the user selects one in ComboBox, you must synchronize to the same folder in TreeView:
Case UMSG of
WM_COMMAND:
IF HiWord (WP) = CBN_SELCHANGE THEN BEGIN
HCOMBO: = Getdlgitem (awnd, idc_combo);
Index: = SendMessage (hcombo, cb_getcursel, 0, 0);
IF index = cb_err dam
CSIDL: = SendMessage (HCOMBO, CB_GETITEMDATA, INDEX, 0); if Csidl <> 555 THEN BEGIN / / CSIDL
SHGETSPECIALFOLDERLOCATION (AWND, CSIDL, PIDL);
SendMessage (awnd, bffm_setselection, 0, longint (pidl));
CotaskMemFree (PIDL);
end
Else Begin // Normal Folder
SETLENGTH (STR, MAX_PATH);
SendMessage (HCOMBO, CB_GETLBText, Index, Longint (PCHAR (STR)));
Str: = PCHAR (STR);
SendMessage (awnd, bffm_setselection, 1, longint (pchar (str));
END;
END;
Since we added ComboBox is a list of Owner-Draw, we must also handle WM_MEASUREITEM and WM_DRAWITEM messages. WM_MEASUREITEM processes relatively simple, because the width of the project for ComboBox does not matter (it automatically determines from the width of the ComboBox itself), we only need to set its height. For simplicity, I use hard-coded methods, of course, based on system settings, careful calculations are also possible (and it should be complete):
WM_MeasureItem:
Begin
PMIS: = PMeasureItemstruct (LP);
IF pmis ^ .ct_comboBox the = ODT_COMBOBOX THEN
PMIS ^ .ItemHeight: = 20;
END;
The PMIS declares as a PMEasureItemStruct structure pointer.
WM_DRAWITEM has more complicated processing. Because for the system-level folder, it must be obtained from the System ImageList, but also the name of the folder from the LPItemidList (not necessarily the file path: For example, C: / windows / desktop is in the shell "is" desktop"). To this end, I added several auxiliary functions to simplify the processing of WM_DRAWITEM:
Function GetNameFromPidl (PIDL: PitemidList): String;
VAR
SFI: SHFILEINFO;
Begin
SHGETFILEINFO (PCHAR (PIDL), 0, SFI, SIZEOF (SFI), SHGFI_DISPLAYNAME OR SHGFI_PIDL);
Result: = StrPas (sfi.szdisplayName);
END;
Function getPathfromPidl (PIDL: PitemidList): String;
VAR
Str: string;
Begin
SETLENGTH (STR, MAX_PATH);
ShgetPathfromidList (PIDL, PCHAR (STR));
Str: = PCHAR (STR);
Result: = STR;
END;
Procedure getSmalliconFromPIDL (PIDL: PitemidList; var IML: himagelist; var index: integer);
VAR
SFI: SHFILEINFO;
Begin
IML: = SHGETFILEINFO (PCHAR (PIDL), 0, SFI, SIZEOF (SFI), SHGFI_SYSICIONINDEX or SHGFI_SMALLICON OR SHGFI_PIDL); Index: = sfi.iicon;
END;
Procedure getSmalliconFromPath (const path: string; var IML: himagelist; var index: integer);
VAR
SFI: SHFILEINFO;
Begin
IML: = SHGETFILEINFO (PCHAR (PATH), 0, SFI, SIZEOF (SFI), SHGFI_SYSICIONDEX OR SHGFI_SMALLICON;
Index: = sfi.iicon;
END;
The code to handle the project is actually very simple in principle, but it is trivial, you must call the SendMessage and Shell API interface functions, and also include the management of GDI objects. I don't plan to carefully explain the following code; the effect of these codes is to add an icon that represents its folder for each project in front of ComboBox.
WM_DRAWITEM:
Begin
PDIS: = PDRAWITEMSTRUCT (LP);
IF pdis ^ .ctltype = ODT_COMBOBOX THEN BEGIN
HCOMBO: = pdis ^ .hwnditem;
IF pdis ^ .Id = $ ffffffff.
CSIDL: = DWORD (SENDMESSAGE (HCOMBO, CB_GETITEMDATA, PDIS ^ .Itemid, 0));
IF (pdis ^ .ItemState and ODS_SELECTED) = ods_selected the begin
FillRect (pdis ^ .HDC, PDIS ^ .rcitem, getsyscolorbrush (color_highlight));
SetTextColor (pdis ^ .hdc, getsyscolor (color_highlighttext);
end
Else Begin
FillRect (pdis ^ .HDC, PDIS ^ .rcitem, getsyscolorbrush);
SetTextColor (pdis ^ .hdc, getsyscolor (color_windowtext);
END;
SetBkmode (pdis ^ .hdc, transparent);
IF csidl <> 555 kiln begin // csidl
SHGETSPECIALFOLDERLOCATION (AWND, CSIDL, PIDL);
Str: = getNameFromPIDL (PIDL);
GetSmalliconFromPIDL (PIDL, HIML, IIMAGE);
ImageList_Draw (HiML, IImage, Pdis ^ .hdc, Pdis ^ .rcitem.Left 2, Pdis ^ .rcitem.top 2, ILD_TRANSPARENT);
INC (PDIS ^ .rcitem.Left, 20);
DrawText (Pdis ^ .HDC, PCHAR (STR), -1, PDIS ^ .rcitem, DT_SINGLINE OR DT_LEFT OR DT_VCENTER;
CotaskMemFree (PIDL);
end
Else Begin // Normal Path
SETLENGTH (STR, MAX_PATH);
SendMessage (hcombo, cb_getlbtext, pdis ^ .Itemid, Longint (Pchar (STR)));
Str: = pChar (STR); GetsmalliconFromPath (Str, HiML, IIMAGE);
ImageList_Draw (HiML, IImage, Pdis ^ .hdc, Pdis ^ .rcitem.Left 2, Pdis ^ .rcitem.top 2, ILD_TRANSPARENT);
INC (PDIS ^ .rcitem.Left, 20);
DrawText (Pdis ^ .HDC, PCHAR (STR), -1, PDIS ^ .rcitem, DT_SINGLINE OR DT_LEFT OR DT_VCENTER;
END;
END;
END;