Win32 对话框助手






4.95/5 (34投票s)
2003 年 4 月 12 日
11分钟阅读

286661

3127
轻松支持对话框调整大小和 ActiveX 控件
1. 背景故事
作为一家软件公司的员工,我被分配了任务,要为一个老式的基于 Win32 的软件程序添加功能。从去年开始,我几乎只使用最新的技术,包括 .NET,讽刺的是,这却让我陷入了处理已有 10 年历史的技术的项目。我的天啊!
因此,我不得不面对 Win32 对话框,以及那些不原生提供按需调整大小功能的全局回调过程,而且我发现 Win32 对话框也不能托管 ActiveX 控件,这真是太糟糕了。所有这一切在我看来都像是为当今的业务需求使用史前工具。而且别让我开始谈论人体工程学的 UI。这些令人惊叹的限制导致了糟糕的管理层技术决策,比如使用 VB 而不是 C++,从而为流血铺平了道路。
所以,一切都始于按照规范实现功能,最终达到了这样一个点:UI 的两个限制是如此糟糕,以至于整个项目看起来就像一个学生项目。我知道 MFC 的 CDialog
s 具有原生的 ActiveX 支持,所以我开始寻找填补这一空白的方法。
事实上,在 MFC 中,ActiveX 支持几乎分散在整个源代码树中。由于将所有 MFC 的混乱带入应用程序非常令人讨厌,并且被强制要求与库进行静态链接(庞大的 DLL),或者必须分发它(MFC42 可能是设置问题中最混乱的东西,你能想到的。谢谢微软升级了(很可能)系统锁定的 MFC DLL 而不更改 DLL 名称),我决定寻找其他方法。其中一种方法是从 MFC 中提取相关代码。我几个小时后就放弃了,这需要如此大量的工作,以至于除了虐待狂的理由外,没有意义。
最初的 MSPRESS ActiveX inside out[^] 书也没有给我任何关于如何完成这个简单任务的思路。碰巧,我偶然发现了 Michael Dunn 的一篇 文章[^] (ATL GUI classes),并且在浏览 MSDEV ATL 源代码几分钟后,发现它提供了我为 WIN32 对话框想要的功能所需的模块。幸运的是,与 MFC 不同,ATL 是(默认)静态链接的,并且大小很小,非常小,出乎意料地提供了一个了不起的框架来开始。不要害怕 ATL 这个缩写,我不会用 COM 包装器来烦你。ATL GUI 类是一个几乎独立的代码,它有足够的实现和简单性来开发不错的轻量级应用程序,而且速度很快。
本文的其余部分将介绍如何为 Win32 对话框添加调整大小和 ActiveX 支持。我们讨论的是通用功能,这并不排除这样一个事实,即大多数时候,当人们想为对话框添加 ActiveX 控件时,通常是因为他们想将 Windows Media Player 添加到他们的应用程序中,或者 Web 浏览器。这些也是我们要解决的问题。
2. 为 WIN32 对话框添加调整大小功能
以下是议程
- 功能定义
- 关于辅助抓手对象的详细信息
- 标准 ATL 对话框对象
- 将抓手集成到 ATL 对话框中
- 初始化步骤
- 我们完成了!
为 Win32 对话框添加调整大小功能,就是允许对话框使用右下角的抓手进行调整大小,同时移动/调整对话框内控件的大小。尽管此功能现在在 W2K 及更高版本的通用控件中是默认的(例如“打开文件”对话框),但如果您想提供它,不仅仅是在 W2K 框上启动应用程序,或者提供一个清单文件。为了说清楚,对话框调整大小功能必须以某种方式编码。
习惯于处理全局 Win32 对话框回调的人们将会对他们有机会使用代码感到震惊,这些代码与标准的 Win32 对话框窗口过程一样小,并且是**完全面向对象的**。这在实践中产生了巨大的差异。例如,可以通过向 ATL 对话框添加一个专用成员(抓手对象)来为其添加调整大小功能。无需处理全局状态变量。
要有一个代码骨架来开始,只需使用 MSDEV 创建一个新的 Win32 应用程序。然后,添加对以下标头的引用
// ATL dialogs
#include <atlbase.h>
#include <atlwin.h>
// ATL ActiveX support
#include <atlcom.h>
#include <atlhost.h>
创建一个新文件并将此代码粘贴到其中
#include "resource.h"
#include "AboutDialog.h"
CComModule _Module;
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
INITCOMMONCONTROLSEX InitCtrls;
InitCtrls.dwICC = ICC_LISTVIEW_CLASSES;
InitCtrls.dwSize = sizeof(INITCOMMONCONTROLSEX);
BOOL bRet = InitCommonControlsEx(&InitCtrls);
_Module.Init(NULL, hInstance, &LIBID_ATLLib);
// show the dialog
// CSampleDialog dlg; // resizable dialog
// CSampleAxDialog dlg; // sample dialog
// CWindowsMediaAxDialog dlg; // windows media player
// CWebBrowserAxDialog dlg; // web browser
CAboutDialog dlg;
dlg.DoModal();
_Module.Term();
return 0;
}
然后粘贴实际的*关于*对话框实现
// About dialog : simple implementation
//
class CAboutDialog : public CDialogImpl<CAboutDialog>
{
public:
enum { IDD = IDD_ABOUT };
BEGIN_MSG_MAP(CAboutDialog)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
MESSAGE_HANDLER(WM_CLOSE, OnClose)
COMMAND_ID_HANDLER(IDOK, OnOK)
COMMAND_ID_HANDLER(IDCANCEL, OnCancel)
END_MSG_MAP()
LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& bHandled)
{
CenterWindow();
return TRUE; // let the system set the focus
}
LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
EndDialog(IDCANCEL);
return 0;
}
LRESULT OnOK(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
EndDialog(IDOK);
return 0;
}
LRESULT OnCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
EndDialog(IDCANCEL);
return 0;
}
};
如您所见,上面的代码将在启动时加载 IDD_ABOUT
对话框资源。只需确保您已准备好这样的对话框。全局 DialogProc
回调被隐藏了,这很好。
如果您需要处理某些对话框消息,只需在消息映射宏中添加一个条目,并为它提供一个处理程序。如果您对某些通知消息感兴趣,请使用 NOTIFY_HANDLER(control_id, notification, func)
宏。有关更多信息,请查阅 atlwin.h
。
现在我们有了对话框框架可以进行操作,让我们克隆这段代码,将对话框命名为 CSampleDialog
,然后简单地将抓手对象添加为类的成员。就像这样
class CSampleDialog : public CDialogImpl<CSampleDialog>
{
protected:
CResizableGrip m_grip;
...
};
可调整大小的抓手是 Paolo Messina 关于 MFC 的优秀文章 CResizableDialog[^] 的结果代码。我从这段代码中提取了唯一相关的部分,添加了我自己的功能,然后使其脱离 MFC,因此它最终成为了一个无需运行时依赖的可重用对象。可调整大小的抓手是一个位于对话框右下角的、看起来像斜线的剥离的滚动条。它模仿了一个标准的调整大小抓手。也就是说,我们在对话框中做的是创建一个抓手实例,然后将对所有对话框控件的引用传递给它,以及自定义调整大小规则。调整大小规则描述了当对话框被调整大小时,给定控件会发生什么。 namely,它应该被移动、调整大小、两者兼有,甚至保持不变(默认规则)?抓手为此提供了一个简单的 API。以下是我们如何在对话框中使用它
LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
m_grip.InitGrip( m_hWnd );
m_grip.ShowSizeGrip();
// allow treeview x-y resize when the dialog is resized
HWND hTreeView = GetDlgItem(IDC_TREE1);
{
CResizableControl *pListDynamics = m_grip.AddDynamicControls();
if (pListDynamics)
{
pListDynamics->Init(hTreeView);
pListDynamics->AllowResizeOnResize();
}
}
// allow OK button x-only move (no resize) when the dialog is resized
HWND hOk = GetDlgItem(IDOK);
{
CResizableControl *pListDynamics = m_grip.AddDynamicControls();
if (pListDynamics)
{
pListDynamics->Init(hOk);
pListDynamics->AllowMoveXOnResize();
}
}
...
return TRUE; // let the system set the focus
}
由于这些初始化步骤是清晰的代码模式,我将为此目的引入辅助宏
#define RX 1
#define RY 2
#define RXY RX | RY
#define MX 4
#define MY 8
#define MXY MX | MY
#define BEGIN_SIZINGRULES(grip, hParent) \
grip.InitGrip( hParent ); \
grip.ShowSizeGrip();
#define ADDRULE(grip, item, rule) \
{ \
HWND hObject##item = GetDlgItem( item ); \
if ( hObject##item ) \
{ \
CResizableControl *pListDynamics = grip.AddDynamicControls(); \
if (pListDynamics) \
{ \
pListDynamics->Init(hObject##item); \
if ((rule)&RX) pListDynamics->AllowResizeXOnResize(); \
if ((rule)&RY) pListDynamics->AllowResizeYOnResize(); \
if ((rule)&MX) pListDynamics->AllowMoveXOnResize(); \
if ((rule)&MY) pListDynamics->AllowMoveYOnResize(); \
} \
} \
}
#define END_SIZINGRULES
#define DORESIZE(grip) \
if (grip.GetSafeHwnd()) \
{ \
grip.UpdateGripPos(); \
grip.MoveAndResize(); \
}
#define MINMAX(x,y) \
LPRECT pRect = (LPRECT) lParam; \
\
int nWidth = pRect->right - pRect->left; \
if (nWidth<x) pRect->right = pRect->left + x; \
\
int nHeight = pRect->bottom - pRect->top; \
if (nHeight<y) pRect->bottom = pRect->top + y;
多亏了宏,上面的代码简化为
LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
BEGIN_SIZINGRULES(m_grip, m_hWnd)
ADDRULE(m_grip, IDC_TREE1, RXY)
ADDRULE(m_grip, IDOK, MX)
END_SIZINGRULES
...
return TRUE; // let the system set the focus
}
然后,我们重写 WM_SIZE
和 WM_SIZING
消息处理程序以提供实际的功能支持。namely,每当对话框被调整大小时,Windows 都会发送 WM_SIZE
。WM_SIZING
也是由 Windows 发送的,它提供了一个独特的机会来按需调整传递的边界矩形,允许我们应用预定义的最小/最大规则。示例如下
BEGIN_MSG_MAP(CSampleDialog)
...
MESSAGE_HANDLER(WM_SIZE, OnSize)
MESSAGE_HANDLER(WM_SIZING, OnSizing)
...
END_MSG_MAP()
// called by framework while resizing
LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
int cx, cy;
cx = LOWORD(lParam);
cy = HIWORD(lParam);
if (m_grip.GetSafeHwnd())
{
m_grip.UpdateGripPos();
m_grip.MoveAndResize();
}
// you can use the following macro instead of the code above :
// DORESIZE(m_grip)
return 0;
}
// called by framework while resizing, to allow min/max bound adjustment
LRESULT OnSizing(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
LPRECT pRect = (LPRECT) lParam;
// min width is 150 pixels
int nWidth = pRect->right - pRect->left;
if ( nWidth < 150 ) pRect->right = pRect->left + 150;
// min height is 150 pixels
int nHeight = pRect->bottom - pRect->top;
if ( nHeight < 150 ) pRect->bottom = pRect->top + 150;
// you can use the following macro instead of the code above :
// MINMAX(150,150)
return 0;
}
最小/最大边界矩形当然与布局以及对话框包含的控件高度相关。这意味着 OnSizing()
的实际实现可能因对话框而异,而 OnSize()
则不同。
在编译代码之前,我们仍然需要做几件事
- 为对话框资源添加**边框调整大小**样式(对话框属性,样式选项卡,边框下拉菜单)
- 添加**剪裁子控件**样式
- 事实上,这些样式会在运行时由抓手对象自动强制应用,所以如果我深入这些细节,那是因为我想让你能够从头开始构建这个过程。运行时强制应用样式涉及类似这样的代码
// force dialog styles ::SetWindowLong(hParent, GWL_STYLE, ::GetWindowLong(hParent, GWL_STYLE) | WS_THICKFRAME | WS_CLIPCHILDREN);
添加最大化按钮,以支持对话框标题的双击(最大化时移除抓手)
- 请记住,如果对话框声明了一个或多个通用控件,则需要在代码中初始化它们(我们还需要链接*comctl32.lib*库)。
编译并运行后,我们得到的结果如下
来自通用框架的可调整大小的对话框
3. 为 WIN32 对话框添加 ActiveX 支持
如果您不知道,ActiveX 控件不受 Win32 对话框支持。如果您尝试将例如 Web 浏览器拖放到 Win32 对话框上,则类向导会中止该过程。事实上,ActiveX 支持需要在内部实现一个 OLE 控件容器,而 Win32 对话框缺少这个。
幸运的是,微软为我们提供了一个 CAxDialogImpl
ATL 类。这个类也是一个 WTL 的根类,但好消息是 Windows Template Library 不是必需的。我们唯一需要从上面粘贴的代码中做的是将 CDialogImpl
替换为 CAxDialogImpl
。
我们仍然需要解决 ActiveX 插入问题,因为类向导不允许我们这样做。有几种方法可以解决这个问题
- 在编辑对话框时,右键单击它,选择“**插入 ActiveX 控件**”,然后选择一个。
- 创建一个假的 MFC 基于对话框的应用程序,向对话框添加一个或多个 ActiveX 控件,然后将生成的*.rc*文件导入到您的应用程序中。您就完成了。
- 手动抓取*.rc*文件中的
CONTROL
标签
这足以与 ActiveX 控件进行交互,但如果我们在大多数用例中,除了使用基于 UI 的属性页外,还需要与它通信,并且实际上需要
- 调用一个或多个方法,或获取一个或多个属性值
- 订阅事件
MFC 类向导提供的调度驱动程序包装器无济于事,因为如前所述,它需要在您的应用程序中包含大部分 MFC 源代码。我们首先要做的是导入 ActiveX 类型库,以便我们可以使用 public
API。导入的类型库由一对文件表示,它们在编译时动态生成,例如在*Debug*文件夹中,*msdxm.tlh*和*msdxm.tli*。我们将处理两个用例
- 播放 Windows Media Player 视频文件
- 使用 Web 浏览器导航 URL
玩转 Windows Media Player
让我们使用此代码导入 Windows Media Player 类型库(智能指针包装器)
// put the following line in your precompiled headers
#import "c:\winnt\system32\msdxm.ocx"
这样做,我们就可以在*Debug\msdxm.tlh + msdxm.tli*文件中获得完整的对象模型。如果你花几分钟浏览它(我知道这很粗鲁!),你会发现有 coclasses 和 interfaces。Coclasses 是我们创建新实例时的入口点,而 interfaces 是我们可以依赖的绑定器。另一方面,ATL CAxDialogImpl
继承了更低级别的类,其中一个类公开了 GetDlgControl(int nID, REFIID iid, /*out*/void** ppDispatch)
访问器,这为我们提供了一种方便的方法来绑定与正在运行的播放器实例(如上所述插入到对话框的*.rc*文件中)的接口。
进行绑定的代码就非常直接了。要设置要播放的特定视频文件名,只需执行以下操作,例如在对话框的 OnInitDialog()
实现中
// Windows media player specific code
MediaPlayer::IMediaPlayerPtr pMediaPlayer = NULL;
HRESULT hr = GetDlgControl(IDC_MEDIAPLAYER1,
__uuidof(MediaPlayer::IMediaPlayer),
(void**)&pMediaPlayer);
pMediaPlayer->FileName = _bstr_t("e:\\videos\\01.avi");
pMediaPlayer->Play();
就是这样! 工作代码在*WindowsMediaAxDialog.h*中提供。
使用 Web 浏览器导航 URL
使用 Web 浏览器与其他 ActiveX 控件一样。但是,由于展示如何接收导航事件很有趣,我们将看到它是如何工作的。
与 Windows Media Player 类似,在*OnInitDialog()*对话框方法中添加此行代码以导入 Web 浏览器类型库
// put the following line in your precompiled headers
#pragma warning( disable : 4192 )
#import "c:\winnt\system32\shdocvw.dll" // web browser control
#import "c:\winnt\system32\mshtml.tlb" // web browser dom
要导航 URL,只需添加此代码(例如在*OnInitDialog()*对话框方法中)
// Web browser specific code
SHDocVw::IWebBrowserAppPtr pWebBrowser = NULL;
HRESULT hr = GetDlgControl(IDC_EXPLORER1,
__uuidof(SHDocVw::IWebBrowserAppPtr),
(void**)&pWebBrowser);
pWebBrowser->Navigate( _bstr_t("https://codeproject.org.cn") );
到目前为止一切顺利。现在我们想接收导航事件的通知,例如当网页从 Web 传输并渲染时。我们需要订阅 Web 浏览器事件源(实际上有两个,出于版本原因,有 DWebBrowserEvents
和 DWebBrowserEvents2
)。为了做到这一点,我们必须枚举 IConnectionPoint
接口(这是事件源的技术名称),并调用 advise()
。也就是说,我们可以使用 ATL EventSink
宏来让我们的生活更轻松。但为了“手动”完成的优雅,让我们用真实的 C++ 代码来做
// subscribe the web browse event source
LPCONNECTIONPOINTCONTAINER pCPC = NULL;
LPCONNECTIONPOINT pCP = NULL;
pWebBrowser->QueryInterface(IID_IConnectionPointContainer, (LPVOID*)&pCPC);
pCPC->FindConnectionPoint(__uuidof(SHDocVw::DWebBrowserEventsPtr), &pCP);
DWORD dwCookie;
pCP->Advise((LPUNKNOWN)&m_events, &dwCookie);
m_events
是一个类成员,它实现了 DWebBrowserEvents
接口,一个 IDispatch 接口
class CWebBrowserAxDialog : public CAxDialogImpl<CWebBrowserAxDialog>
{
protected:
DWebBrowserEventsImpl m_events;
...
}
class DWebBrowserEventsImpl : public DWebBrowserEvents
{
// IUnknown methods
STDMETHOD(QueryInterface)(REFIID riid, LPVOID* ppv);
STDMETHOD_(ULONG, AddRef)();
STDMETHOD_(ULONG, Release)();
// IDispatch methods
STDMETHOD(GetTypeInfoCount)(UINT* pctinfo);
STDMETHOD(GetTypeInfo)(UINT iTInfo,
LCID lcid,
ITypeInfo** ppTInfo);
STDMETHOD(GetIDsOfNames)(REFIID riid,
LPOLESTR* rgszNames,
UINT cNames,
LCID lcid,
DISPID* rgDispId);
STDMETHOD(Invoke)(DISPID dispIdMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS __RPC_FAR *pDispParams,
VARIANT __RPC_FAR *pVarResult,
EXCEPINFO __RPC_FAR *pExcepInfo,
UINT __RPC_FAR *puArgErr);
// events
HRESULT BeforeNavigate (
_bstr_t URL,
long Flags,
_bstr_t TargetFrameName,
VARIANT * PostData,
_bstr_t Headers,
VARIANT_BOOL * Cancel );
HRESULT NavigateComplete ( _bstr_t URL );
HRESULT StatusTextChange ( _bstr_t Text );
HRESULT ProgressChange (
long Progress,
long ProgressMax );
HRESULT DownloadComplete();
HRESULT CommandStateChange (
long Command,
VARIANT_BOOL Enable );
HRESULT DownloadBegin ();
HRESULT NewWindow (
_bstr_t URL,
long Flags,
_bstr_t TargetFrameName,
VARIANT * PostData,
_bstr_t Headers,
VARIANT_BOOL * Processed );
HRESULT TitleChange ( _bstr_t Text );
HRESULT FrameBeforeNavigate (
_bstr_t URL,
long Flags,
_bstr_t TargetFrameName,
VARIANT * PostData,
_bstr_t Headers,
VARIANT_BOOL * Cancel );
HRESULT FrameNavigateComplete (
_bstr_t URL );
HRESULT FrameNewWindow (
_bstr_t URL,
long Flags,
_bstr_t TargetFrameName,
VARIANT * PostData,
_bstr_t Headers,
VARIANT_BOOL * Processed );
HRESULT Quit (
VARIANT_BOOL * Cancel );
HRESULT WindowMove ( );
HRESULT WindowResize ( );
HRESULT WindowActivate ( );
HRESULT PropertyChange (
_bstr_t Property );
// members
// any time a IWebBrowser instance is needed
CWebBrowserAxDialog *m_cpParent;
public:
void SetParent(CWebBrowserAxDialog *pParent) { m_cpParent = pParent; }
};
剩下的就是实现 IDispatch::Invoke()
,这是所有已订阅事件的实际入口点。根据约定,在 Invoke
实现中,我们的工作是将事件分派为适当的方法调用。在示例中,我们只实现了 OnBeforeNavigate
事件的分派,这是程序员主要感兴趣的事件之一。实现代码如下
HRESULT __stdcall DWebBrowserEventsImpl::Invoke(DISPID dispIdMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS __RPC_FAR *pDispParams,
VARIANT __RPC_FAR *pVarResult,
EXCEPINFO __RPC_FAR *pExcepInfo,
UINT __RPC_FAR *puArgErr)
{
// process OnBeforeNavigate
if (dispIdMember == DISPID_BEFORENAVIGATE)
{
// call BeforeNavigate
// (parameters are on stack, thus on reverse order)
BeforeNavigate( /*url*/ _bstr_t( pDispParams->rgvarg[5].bstrVal ),
0,
_bstr_t( pDispParams->rgvarg[3].bstrVal ),
NULL,
_bstr_t(""),
NULL);
}
else if (dispIdMember == DISPID_NAVIGATECOMPLETE)
{
NavigateComplete( _bstr_t( pDispParams->rgvarg[0].bstrVal ) );
}
else
{
... // implement all event handlers of interest to you
}
return NOERROR;
}
就是这样。工作代码在*WebBrowserAxDialog.h*和*WebBrowserAxDialog.cpp*中提供。
最后但同样重要的是,以下是如何获取当前的 HTML 文档:(通过导入 mshtml 类型库实现)
HRESULT DWebBrowserEventsImpl::NavigateComplete ( _bstr_t URL )
{
SHDocVw::IWebBrowserAppPtr pWebBrowser = NULL;
HRESULT hr = m_cpParent->GetDlgControl(IDC_EXPLORER1,
__uuidof(SHDocVw::IWebBrowserAppPtr),
(void**)&pWebBrowser);
// get the HTML document
MSHTML::IHTMLDocument2Ptr doc( pWebBrowser->Document );
MSHTML::IHTMLElementPtr htmlbody( doc->body );
BSTR content = NULL;
htmlbody->get_innerHTML(&content);
_bstr_t bcontent(content);
return S_OK;
}
顺便说一句,有一个很棒的 文章[^],开发人员经常参考它,当他们试图将 Web 浏览器集成到 MFC 对话框中时。Dino 很难将 CHtmlView
类(CView
派生类)当作一个简单的对话框控件来使用。最终,这会奏效,尽管这需要大量的开销,包括降低 MFC 的文档/视图模型的性能。
更新历史
- 2003 年 4 月 12 日 - 初次发布
- 2003 年 4 月 19 日 - 更新
- 消除了闪烁(
WS_CLIPCHILDREN
样式) - 添加了辅助宏
- 添加了 ATL7 支持
- 消除了闪烁(
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。