範例︰實作屬性頁
Visual Studio 2019 及更新版本中未提供 ATL 屬性頁精靈。
此範例示範如何建置可顯示 (並可讓您變更) 文件類別介面屬性的屬性頁。
此範例是以 ATLPages 範例為基礎。
若要完成此範例,您將會:
使用 [新增類別] 對話方塊和 ATL 屬性頁精靈,新增 ATL 屬性頁類別。
加入
Document
介面有趣屬性的新控制項以編輯對話方塊資源。新增訊息處理常式以保留通知使用者所做變更的屬性頁網站。
在 Housekeeping 區段中加入一些
#import
陳述式和 Typedef。覆寫 IPropertyPageImpl::SetObjects 可驗證傳遞至屬性頁的物件。
覆寫 IPropertyPageImpl::Activate 可初始化屬性頁的介面。
覆寫 IPropertyPageImpl::Apply 可將物件更新為最新的屬性值。
建立簡單的協助程式物件可顯示屬性頁。
建立巨集,此巨集將測試屬性頁。
新增 ATL 屬性頁類別
首先,為 DLL 伺服器建立一個稱為 ATLPages7
的新 ATL 專案。 現在,使用 ATL 屬性頁精靈產生屬性頁。 為屬性頁提供一個 DocProperties 的簡短名稱,然後切換到 [字串]頁面,以設定屬性頁面專屬的項目,如下表所示。
項目 | 值 |
---|---|
標題 | TextDocument |
Doc 字串 | VCUE TextDocument 屬性 |
Helpfile | <空白> |
您在這個精靈頁面上設定的值將會在呼叫 IPropertyPage::GetPageInfo
時,傳回至屬性頁容器。 字串在此之後發生的變化取決於容器,但通常用來向使用者識別您的頁面。 Title 通常會在出現您頁面上方的索引標籤中,而 Doc 字串則可能會顯示在狀態列或工具提示中 (但標準屬性框架完全不會使用此字串)。
注意
精靈會將您在此處設定的字串儲存為您專案中的字串資源。 如果您需要在產生頁面的程式碼之後變更此資訊,您可以使用資源編輯器,輕鬆地編輯這些字串。
按一下 [確定],讓精靈產生屬性頁。
編輯對話方塊資源
既然已經產生屬性頁,您必須將幾個控制項新增至代表您頁面的對話方塊資源。 新增編輯方塊、靜態文字控制項以及核取方塊,並設定其識別碼,如下所示:
這些控制項將用來顯示文件的檔案名稱及其唯讀狀態。
注意
對話方塊資源不包含框架或命令按鈕,也沒有您可能預期的索引標籤式外觀。 屬性頁框架會提供這些功能,例如,透過呼叫 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 或 ProgID。 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 屬性頁的 ProgID,以及目前在 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
當您執行此巨集時,將會顯示屬性頁,其中會顯示目前作用中之文字文件的檔案名稱和唯讀狀態。 文件的唯讀狀態只會反映是否能夠寫入至開發環境中的文件;它不會影響磁碟上檔案的唯讀屬性。