示例:实现属性页

ATL 属性页向导不适用于 Visual Studio 2019 及更高版本。

此示例展示了如何生成属性页来显示(并允许更改)文档类接口的属性。

此示例基于 ATLPages 示例

若要完成此示例,请执行以下操作:

添加 ATL 属性页类

首先,为名为“ATLPages7”的 DLL 服务器新建 ATL 项目。 现在使用 ATL 属性页向导来生成属性页。 为属性页命名“DocProperties”作为“短名称”,然后切换到“字符串”页,以设置属性页专用项,如下表所示。

游戏 TextDocument
文档字符串 VCUE TextDocument 属性
帮助文件 <空白>

调用 IPropertyPage::GetPageInfo 时,在向导的此页上设置的值会返回给属性页容器。 此后对字符串执行的操作取决于容器,但它们通常用于向用户标识属性页。 “标题”通常显示在属性页上方的选项卡中,“文档字符串”可能显示在状态栏或工具提示中(尽管标准属性框架根本不使用此字符串)。

注意

此处设置的字符串由向导存储为项目中的字符串资源。 如果需要在生成属性页代码后更改此信息,可以使用资源编辑器轻松编辑这些字符串。

单击“确定”,让向导生成属性页。

编辑对话框资源

至此,已生成属性页。需要将一些控件添加到表示属性页的对话框资源中。 添加编辑框、静态文本控件和复选框,并设置它们的 ID,如下所示:

Screenshot of a dialog resource in the visual editor.

这些控件用于显示文档的文件名及其只读状态。

注意

对话框资源既没有框架或命令按钮,也没有你可能期望的选项卡式外观。 这些功能由属性页框架提供。例如,通过调用 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 执行方法调用。

注意

DocumentFullName 公开为只读属性。 若要根据属性页更改来更新文档的文件名,必须使用 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

在你运行此宏后,属性页显示,其中包括当前活动文本文档的文件名和只读状态。 文档的只读状态仅反映能否在开发环境中向文档写入内容;并不影响磁盘上文件的只读属性。

另请参阅

属性页
ATLPages 示例