Window SubClassing另类运用(之二)

类别:Delphi 点击:0 评论:0 推荐:
 

Window SubClassing另类运用(之二)


 


 


你大概已经熟悉通用对话框(打开/保存文件,选择字体/颜色,以及查找和替换)的使用,不过你是否了解如何调用“选择文件夹”对话框呢?如果答案是否的话,你可以先看看一个简单的例子,籍以做个热身。如果你自认为已经了解它的话,可以跳过下面这一段。


 


要调用“选择文件夹”对话框,和其他通用对话框所使用的方法非常类似:一个结构(BROWSEINFO)加一个函数(SHBrowseForFolder)即可。请看代码:


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 then begin


     SetLength(str, MAX_PATH);


     SHGetPathFromIDList(pidl, PChar(str));


     str := PChar(str);


     Caption := str;


     CoTaskMemFree(pidl);


  end;


end;


SHBrowseForFolder返回一个LPITEMIDLIST,你需要手动将它转换成一个实际的文件路径(除非你选择的是回收站和控制面板这样的虚拟路径)。最后还要用Shell API把获得的pidl释放。上述代码中,BrowseCallback是一个自己编写的回调函数,如果不想处理回调的话,可以将它设置为nil。我还是处理了这个函数,因为我需要它的一些功能,如下:


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 then begin


           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;


BrowseCallback函数可以接受一些通知消息,例如上面列出的BFFM_SELCHANGED,当用户在文件夹列表中选择了另外一个项目的时候就会触发,程序员可以用另外一些消息(如BFFM_SETSTATUSTEXT)更新对话框其他相应的部分。


 


 


对SHBrowseForFolder的介绍说这么多也就足够了。不过,我对于这样单调的界面并不满意。一个最直接的想法就是:希望在对话框中添加一个列表,其中列出一些常用的文件夹供用户选择,而不需要每次都在“庭院深深”的层次树中一次再一次的Click。这又是一个使用SubClass的好地方。还记得在本文的系列之一中我提到的吗?要使用SubClass技术,充分必要条件就是获得一个窗口的句柄。非常幸运,这里我们有很简单的办法能够得到这个句柄,因为对话框初始化成功后会向上述的回调函数发送BFFM_INITIALIZED通知,我们的SubClass工作就在这里完成。


 


在上述的BrowseCallback函数中添加如下的Message Dispatcher:


  case uMsg of


    BFFM_INITIALIZED:


      begin


        OldBrowseProc := TWindowProc(GetWindowLong(AWnd, GWL_WNDPROC));


        SetWindowLong(AWnd, GWL_WNDPROC, LongInt(@NewBrowseProc));


        AdjustDlg(AWnd);


end;


 


其中,OldBrowseProc是在implementation部分声明的变量:


var


  OldBrowseProc : TWindowProc = nil;


 


而NewBrowseProc和AdjustDlg都是自己编写的函数,它们都比较长,我将分段讲述它们的内容。


 


先来看AdjustDlg的工作。它的任务是向对话框中添加一个组合框(Combo Box),并且向其中添加几个项目。听起来很简单,不过有许多琐碎的工作必须要做。因为我们是在对系统定义的窗口进行SubClass,所以VCL在这里基本上帮不上什么忙:我们必须大量使用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, 'SysTreeView32')=0 then begin


       Found := True;


       Break;


    end;


    wnd := GetWindow(wnd, GW_HWNDNEXT);


  end;


  if not Found then Exit;


为了能够让插入的ComboBox和其他窗口控件的布局协调一致,首先需要找到用来显示文件夹的TreeView窗口。我的计划是:让ComboBox占据TreeView原来的位置(当然它的高度要比TreeView小得多),然后,包括TreeView在内的其他窗口依次下移。下面是实现代码:


  // 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_DROPDOWNLIST 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_NOSIZE 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);


 


如果你过去很少用API写程序,那么这些代码可能让你看得有点头晕。基本上上述程序完成如下的工作:


(1)计算TreeView在窗口中的位置;


(2)建立ComboBox窗口,并基于TreeView的位置将它放置到合理的地方;


(3)将ComboBox的字体设置为和整个窗体的字体相同(这一步是必要的,否则显示的效果会很难看);


(4)为TreeView的窗口风格添加TVS_SHOWSELALWAYS位,从而在焦点移动到ComboBox的时候,仍然可以明显的观察到TreeView中究竟选中了哪个项目;


(5)将窗口中的其他控件依次下移,从而为ComboBox腾出必要的空间;


(6)将窗口本身的高度也略微放大,从而适应添加ComboBox以后的大小。


 


下一步就是向ComboBox中增加一些表项,否则的话它就是一个鸡肋。我决定添加两种项目:(1)系统中的某些特殊路径,这些路径可以通过SHGetSpecialFolderLocation获得;(2)通常的文件路径。为了让代码简洁一些,我增加了一个辅助函数:


 


  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;


 


然后在AdjustDlg函数的末尾添加如下的代码:


  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);


这里用555并没有什么特别的意义。我本来想用0来标志普通文件夹,但后来发现CSIDL_DESKTOP正是定义为0,所以必须用其他数字来区分。555是我信手写的,你当然可以用别的数字,只要注意不要和预定义的CSIDL常量冲突即可。


 


AdjustDlg函数的内容就这么多。接下来是NewBrowseProc函数的内容,它的基本结构如下:


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;


 


在NewBrowseProc中必须处理几条消息。第一个就是用户在ComboBox中选择一项的时候,在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 then Exit;


         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;


 


由于我们添加的ComboBox是一个自绘风格(Owner-Draw)的列表,所以我们还必须处理WM_MEASUREITEM和WM_DRAWITEM消息。WM_MEASUREITEM的处理相对简单,因为对于ComboBox来说项目的宽度无所谓(它自动由ComboBox本身的宽度来决定),我们只需要设置它的高度即可。为了简化起见,我用了硬编码的方法,当然基于系统设置进行仔细的计算也是可行的(而且完全应该):


    WM_MEASUREITEM:


      begin


        pmis := PMEASUREITEMSTRUCT(lp);


        if pmis^.CtlType=ODT_COMBOBOX then


           pmis^.itemHeight := 20;


end;


其中pmis声明为一个PMEASUREITEMSTRUCT结构指针。


 


WM_DRAWITEM的处理要复杂的多。因为对于系统级的文件夹,必须从System ImageList中获得它的图标,而且还要从LPITEMIDLIST取得文件夹的名称(不一定是文件路径:比如,c:\windows\desktop在Shell中的名称是“桌面”)。为此我添加了几个辅助函数,用来简化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_SYSICONINDEX 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_SYSICONINDEX or SHGFI_SMALLICON);


  index := sfi.iIcon;


end;


 


处理项目绘制的代码其实从原理上来讲非常简单,但是比较琐碎,必须大量调用SendMessage和Shell API接口函数,还包括GDI对象的管理。我不打算仔细解释下面这些代码;这些代码的效果就是在ComboBox中为每一个项目前面添加一个代表其文件夹的图标。


    WM_DRAWITEM:


      begin


        pdis := PDRAWITEMSTRUCT(lp);


        if pdis^.CtlType=ODT_COMBOBOX then begin


           hCombo := pdis^.hwndItem;


           if pdis^.itemID=$ffffffff then Exit;


           csidl := DWORD(SendMessage(hCombo, CB_GETITEMDATA, pdis^.itemID, 0));


if (pdis^.itemState and ODS_SELECTED)=ODS_SELECTED then begin


              FillRect(pdis^.hDC, pdis^.rcItem, GetSysColorBrush(COLOR_HIGHLIGHT));


              SetTextColor(pdis^.hDC, GetsysColor(COLOR_HIGHLIGHTTEXT));


           end


           else begin


              FillRect(pdis^.hDC, pdis^.rcItem, GetSysColorBrush(COLOR_WINDOW));


              SetTextColor(pdis^.hDC, GetSysColor(COLOR_WINDOWTEXT));


           end;


           SetBkMode(pdis^.hDC, TRANSPARENT);


           if csidl<>555 then 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_SINGLELINE 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_SINGLELINE or DT_LEFT or DT_VCENTER);


           end;


        end;


end;


 


 


 

本文地址:http://com.8s8s.com/it/it5666.htm