示例:实现属性页
ATL 属性页向导不适用于 Visual Studio 2019 及更高版本。
此示例展示了如何生成属性页来显示(并允许更改)文档类接口的属性。
此示例基于 ATLPages 示例。
若要完成此示例,请执行以下操作:
使用 ATL 属性页向导中的“添加类”对话框来添加 ATL 属性页类。
通过为受关注的
Document
接口的属性添加新控件,编辑对话框资源。添加消息处理程序,以确保属性页网站随时掌握用户所做的更改。
在“保养工作”部分中,添加一些
#import
语句和 typedef。重写 IPropertyPageImpl::SetObjects,以验证对象是否要传递到属性页。
重写 IPropertyPageImpl::Activate,以初始化属性页的接口。
重写 IPropertyPageImpl::Apply,以使用最新属性值来更新对象。
通过创建简单的帮助程序对象,显示属性页。
添加 ATL 属性页类
首先,为名为“ATLPages7
”的 DLL 服务器新建 ATL 项目。 现在使用 ATL 属性页向导来生成属性页。 为属性页命名“DocProperties”作为“短名称”,然后切换到“字符串”页,以设置属性页专用项,如下表所示。
项 | 值 |
---|---|
游戏 | TextDocument |
文档字符串 | VCUE TextDocument 属性 |
帮助文件 | <空白> |
调用 IPropertyPage::GetPageInfo
时,在向导的此页上设置的值会返回给属性页容器。 此后对字符串执行的操作取决于容器,但它们通常用于向用户标识属性页。 “标题”通常显示在属性页上方的选项卡中,“文档字符串”可能显示在状态栏或工具提示中(尽管标准属性框架根本不使用此字符串)。
注意
此处设置的字符串由向导存储为项目中的字符串资源。 如果需要在生成属性页代码后更改此信息,可以使用资源编辑器轻松编辑这些字符串。
单击“确定”,让向导生成属性页。
编辑对话框资源
至此,已生成属性页。需要将一些控件添加到表示属性页的对话框资源中。 添加编辑框、静态文本控件和复选框,并设置它们的 ID,如下所示:
这些控件用于显示文档的文件名及其只读状态。
注意
对话框资源既没有框架或命令按钮,也没有你可能期望的选项卡式外观。 这些功能由属性页框架提供。例如,通过调用 OleCreatePropertyFrame 创建的属性页框架。
添加消息处理程序
在控件就位后,可以添加消息处理程序,用于在任一控件的值更改时更新属性页的脏状态:
BEGIN_MSG_MAP(CDocProperties)
COMMAND_HANDLER(IDC_NAME, EN_CHANGE, OnUIChange)
COMMAND_HANDLER(IDC_READONLY, BN_CLICKED, OnUIChange)
CHAIN_MSG_MAP(IPropertyPageImpl<CDocProperties>)
END_MSG_MAP()
// Respond to changes in the UI to update the dirty status of the page
LRESULT OnUIChange(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
wNotifyCode; wID; hWndCtl; bHandled;
SetDirty(true);
return 0;
}
此代码通过调用 IPropertyPageImpl::SetDirty 来通知属性页网站属性页已更改,从而响应编辑控件或复选框的更改。 属性页网站的响应方式通常为,在属性页框架上启用或禁用“应用”按钮。
注意
在你自己的属性页中,你可能需要精确跟踪用户更改了哪些属性,以免更新尚未更改的属性。 此示例实现了相应代码,具体方式为跟踪原始属性值,并将它们与应用更改后 UI 中的当前值进行比较。
保养工作
现在,将一些 #import
语句添加到 DocProperties.h 中,让编译器知晓 Document
接口:
// MSO.dll
#import <libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52> version("2.2") \
rename("RGB", "Rgb") \
rename("DocumentProperties", "documentproperties") \
rename("ReplaceText", "replaceText") \
rename("FindText", "findText") \
rename("GetObject", "getObject") \
raw_interfaces_only
// dte.olb
#import <libid:80CC9F66-E7D8-4DDD-85B6-D9E6CD0E93E2> \
inject_statement("using namespace Office;") \
rename("ReplaceText", "replaceText") \
rename("FindText", "findText") \
rename("GetObject", "getObject") \
rename("SearchPath", "searchPath") \
raw_interfaces_only
还需要引用 IPropertyPageImpl
基类;将以下 typedef
添加到 CDocProperties
类中:
typedef IPropertyPageImpl<CDocProperties> PPGBaseClass;
重写 IPropertyPageImpl::SetObjects
需要重写的第一个 IPropertyPageImpl
方法是 SetObjects。 随后将添加代码,以检查是否只传递了一个对象,且它是否支持所需的 Document
接口:
STDMETHOD(SetObjects)(ULONG nObjects, IUnknown** ppUnk)
{
HRESULT hr = E_INVALIDARG;
if (nObjects == 1)
{
CComQIPtr<EnvDTE::Document> pDoc(ppUnk[0]);
if (pDoc)
hr = PPGBaseClass::SetObjects(nObjects, ppUnk);
}
return hr;
}
注意
最好对此页仅支持一个对象,因为这样用户可以设置对象的文件名(也就是说,任一位置上只能有一个文件)。
重写 IPropertyPageImpl::Activate
下一步是在首次创建属性页时用基础对象的属性值初始化属性页。
在此示例中,应将以下成员添加到类中,因为当属性页的用户应用更改时,你还将使用初始属性值进行比较:
CComBSTR m_bstrFullName; // The original name
VARIANT_BOOL m_bReadOnly; // The original read-only state
Activate 方法的基类实现负责创建对话框及其控件,所以你可以重写此方法,并在调用基类后添加你自己的初始化:
STDMETHOD(Activate)(HWND hWndParent, LPCRECT prc, BOOL bModal)
{
// If we don't have any objects, this method should not be called
// Note that OleCreatePropertyFrame will call Activate even if
// a call to SetObjects fails, so this check is required
if (!m_ppUnk)
return E_UNEXPECTED;
// Use Activate to update the property page's UI with information
// obtained from the objects in the m_ppUnk array
// We update the page to display the Name and ReadOnly properties
// of the document
// Call the base class
HRESULT hr = PPGBaseClass::Activate(hWndParent, prc, bModal);
if (FAILED(hr))
return hr;
// Get the EnvDTE::Document pointer
CComQIPtr<EnvDTE::Document> pDoc(m_ppUnk[0]);
if (!pDoc)
return E_UNEXPECTED;
// Get the FullName property
hr = pDoc->get_FullName(&m_bstrFullName);
if (FAILED(hr))
return hr;
// Set the text box so that the user can see the document name
USES_CONVERSION;
SetDlgItemText(IDC_NAME, CW2CT(m_bstrFullName));
// Get the ReadOnly property
m_bReadOnly = VARIANT_FALSE;
hr = pDoc->get_ReadOnly(&m_bReadOnly);
if (FAILED(hr))
return hr;
// Set the check box so that the user can see the document's read-only status
CheckDlgButton(IDC_READONLY, m_bReadOnly ? BST_CHECKED : BST_UNCHECKED);
return hr;
}
此代码使用 Document
接口的 COM 方法来获取你感兴趣的属性。 然后,它使用 CDialogImpl 提供的 Win32 API 包装器及其基类,向用户显示属性值。
重写 IPropertyPageImpl::Apply
如果用户要将更改应用于对象,属性页网站会调用 Apply 方法。 此时要做的操作与 Activate
中的代码相反:Activate
是从对象获取值,并将值推送到属性页上的控件;而 Apply
则是从属性页上的控件获取值,并将值推送到对象。
STDMETHOD(Apply)(void)
{
// If we don't have any objects, this method should not be called
if (!m_ppUnk)
return E_UNEXPECTED;
// Use Apply to validate the user's settings and update the objects'
// properties
// Check whether we need to update the object
// Quite important since standard property frame calls Apply
// when it doesn't need to
if (!m_bDirty)
return S_OK;
HRESULT hr = E_UNEXPECTED;
// Get a pointer to the document
CComQIPtr<EnvDTE::Document> pDoc(m_ppUnk[0]);
if (!pDoc)
return hr;
// Get the read-only setting
VARIANT_BOOL bReadOnly = IsDlgButtonChecked(IDC_READONLY) ? VARIANT_TRUE : VARIANT_FALSE;
// Get the file name
CComBSTR bstrName;
if (!GetDlgItemText(IDC_NAME, bstrName.m_str))
return E_FAIL;
// Set the read-only property
if (bReadOnly != m_bReadOnly)
{
hr = pDoc->put_ReadOnly(bReadOnly);
if (FAILED(hr))
return hr;
}
// Save the document
if (bstrName != m_bstrFullName)
{
EnvDTE::vsSaveStatus status;
hr = pDoc->Save(bstrName, &status);
if (FAILED(hr))
return hr;
}
// Clear the dirty status of the property page
SetDirty(false);
return S_OK;
}
注意
此实现开头针对 m_bDirty 的检查是初始检查,以免在多次调用 Apply
时不必要地更新对象。 此外,还有针对每个属性值的检查,以确保只有更改才会导致对 Document
执行方法调用。
注意
Document
将 FullName
公开为只读属性。 若要根据属性页更改来更新文档的文件名,必须使用 Save
方法来保存名称不同的文件。 因此,属性页中的代码不必将自己限制为只能获取或设置属性。
显示属性页
若要显示此页,需要创建简单的帮助程序对象。 帮助程序对象提供可简化 OleCreatePropertyFrame
API 的方法,用于显示连接到单一对象的一个属性页。 此帮助程序将被设计为可以在 Visual Basic 中使用。
使用“添加类”对话框和 ATL 简单对象向导来生成新类,并使用“Helper
”作为它的短名称。 创建类后,立即添加如下表所示的方法。
项 | 值 |
---|---|
方法名 | ShowPage |
参数 | [in] BSTR bstrCaption, [in] BSTR bstrID, [in] IUnknown* pUnk |
bstrCaption 参数是要显示为对话框标题的描述文字。 bstrID 参数是表示要显示的属性页的 CLSID 或编程 ID 的字符串。 pUnk 参数是属性由属性页配置的对象的 IUnknown
指针。
按如下所示实现方法:
STDMETHODIMP CHelper::ShowPage(BSTR bstrCaption, BSTR bstrID, IUnknown* pUnk)
{
if (!pUnk)
return E_INVALIDARG;
// First, assume bstrID is a string representing the CLSID
CLSID theCLSID = {0};
HRESULT hr = CLSIDFromString(bstrID, &theCLSID);
if (FAILED(hr))
{
// Now assume bstrID is a ProgID
hr = CLSIDFromProgID(bstrID, &theCLSID);
if (FAILED(hr))
return hr;
}
// Use the system-supplied property frame
return OleCreatePropertyFrame(
GetActiveWindow(), // Parent window of the property frame
0, // Horizontal position of the property frame
0, // Vertical position of the property frame
bstrCaption, // Property frame caption
1, // Number of objects
&pUnk, // Array of IUnknown pointers for objects
1, // Number of property pages
&theCLSID, // Array of CLSIDs for property pages
NULL, // Locale identifier
0, // Reserved - 0
NULL // Reserved - 0
);
}
创建宏
生成项目后,便能使用简单的宏来测试属性页和帮助程序对象,此宏可以在 Visual Studio 开发环境中创建和运行。 此宏创建帮助程序对象,然后使用 DocProperties 属性页的编程 ID 和 Visual Studio 编辑器中当前活动文档的 IUnknown
指针来调用它的 ShowPage
方法。 此宏所需的代码如下所示:
Imports EnvDTE
Imports System.Diagnostics
Public Module AtlPages
Public Sub Test()
Dim Helper
Helper = CreateObject("ATLPages7.Helper.1")
On Error Resume Next
Helper.ShowPage( ActiveDocument.Name, "ATLPages7Lib.DocumentProperties.1", DTE.ActiveDocument )
End Sub
End Module
在你运行此宏后,属性页显示,其中包括当前活动文本文档的文件名和只读状态。 文档的只读状态仅反映能否在开发环境中向文档写入内容;并不影响磁盘上文件的只读属性。