Window SubClassing另类运用(之二)

    技术2022-05-11  61

     

    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;

     

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

     

    先来看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_MEASUREITEMWM_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/desktopShell中的名称是“桌面”)。为此我添加了几个辅助函数,用来简化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;

     

    处理项目绘制的代码其实从原理上来讲非常简单,但是比较琐碎,必须大量调用SendMessageShell 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;

     

     

     


    最新回复(0)