前情提要:

Froser:COM编程攻略(十四 连接点与其ATL实现)​zhuanlan.zhihu.com

这一篇主要来说一下持久化和结构化存储。

本篇文章和上一篇没有关系。

一、什么是持久化(Persist)

想想一下,如果我创建了一个对象,此时我需要关闭我的程序了,下一次打开我希望我的对象仍然保持上一次的状态,我应该怎么做?普遍的做法就是,将它的状态序列化,作为一串字节流写入硬盘中。将对象的状态保存下来,并且可以支持恢复成先前的状态,这样的过程就叫做持久化,支持持久化的对象叫做可持久化的对象。

那么大家大概也可以猜到了,如果一个对象能持久化,那么至少它要能够写入数据(Save),以及从读取数据(Load)。写入就是将自身序列化,读取则是将自身反序列化。

二、COM中的持久化:IPersist接口

微软对COM对象提供了一套接口来实现持久化,这套一口是IPersist系列接口。

我们所用到的这系列接口包括:

IPersist
IPersistStream
IPersistStreamInit
IPersistMemory
IPersistStorage
IPersistFile
IPersistPropertyBag
IPersistMoniker
IPersistHistory

我们先从IPersist开始介绍,然后介绍这一套家族中最常用的几个。

IPersist接口是所有持久化接口的基类,它定义如下:

interface IPersist : IUnknown
{HRESULT GetClassID([out] CLSID* pClassID);
}

它只有GetClassID这一个接口。也就是说,如果一个类是可以持久化的,那么它必须提供自己的类型信息。想象一下,如果你要从数据流读取一个可持久化的对象,你应该如何创建它?一般情况下,我们是通过CoCreateInstance(Ex)来创建,那么它的参数之一的CLSID,就可以从这里来获取。

IPersistStream在IPersist的基础上,提供了读取、写入流的操作,如果一个对象实现了IPersistStream,那么它可以将自己的状态序列化进流,也可以从流还原自身状态。

interface IPersistStream : IPersist
{// 是否是标脏的?HRESULT IsDirty(void);// 读取一个流,还原自己状态HRESULT Load([in, unique] IStream *pStm);// 将自己的状态保存进流HRESULT Save([in, unique] IStream *pStm, [in] BOOL fClearDirty);// 保存自己的状态需要多少字节?HRESULT GetSizeMax([out] ULARGE_INTEGER *pcbSize);
}

此外,它还有一个增强版本IPersistStreamInit:

interface IPersistStreamInit : IPersist
{HRESULT IsDirty(void);HRESULT Load([in] LPSTREAM pStm);HRESULT Save([in] LPSTREAM pStm,[in] BOOL fClearDirty);HRESULT GetSizeMax([out] ULARGE_INTEGER * pCbSize);HRESULT InitNew(void);
}

它只增加了一个InitNew方法,表示将这个类初始化成最原始的样子(可以理解为重置,reset)。

流在COM中用IStream接口表示。我们之前在讲套间的时候,提到过这个IStream:

Froser:COM编程攻略(十二 套间Apartment)​zhuanlan.zhihu.com

IStream继承于ISequentialStream(顺序流),ISequentialStream暴露的是最基本的读取和写入的接口:

interface ISequentialStream : IUnknown
{// 从当前位置读取指定大小的数据HRESULT Read([out, size_is(cb), length_is(*pcbRead)] void *pv,[in] ULONG cb,[out] ULONG *pcbRead);// 从当前位置写入指定大小的位置HRESULT Write([in, size_is(cb)] void const *pv,[in] ULONG cb,[out] ULONG *pcbWritten);
}

顺序流并没有提供更多的功能,例如重置位置等方法,这些功能在IStream中暴露了出来:

interface IStream : ISequentialStream
{// 设置一个位置[local]HRESULT Seek([in] LARGE_INTEGER dlibMove,[in] DWORD dwOrigin,[annotation("__out_opt")] ULARGE_INTEGER *plibNewPosition);// 更改流的大小HRESULT SetSize([in] ULARGE_INTEGER libNewSize);// 从某个位置拷贝数据到另外一个流[local]HRESULT CopyTo([in, unique] IStream *pstm,[in] ULARGE_INTEGER cb,[annotation("__out_opt")] ULARGE_INTEGER *pcbRead,[annotation("__out_opt")] ULARGE_INTEGER *pcbWritten);// 提交事务HRESULT Commit([in] DWORD grfCommitFlags);// 回滚事务HRESULT Revert();// 锁定某个区域HRESULT LockRegion([in] ULARGE_INTEGER libOffset,[in] ULARGE_INTEGER cb,[in] DWORD dwLockType);// 解锁某个区域HRESULT UnlockRegion([in] ULARGE_INTEGER libOffset,[in] ULARGE_INTEGER cb,[in] DWORD dwLockType);// 获取当前流的信息HRESULT Stat([out] STATSTG *pstatstg,[in] DWORD grfStatFlag);// 克隆一个流到另外一个流HRESULT Clone([out] IStream **ppstm);
}

你不用自己实现这些方法,因为微软提供了一个函数CreateStreamOnHGlobal,可以创建好微软我们实现的IStream对象:

https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-createstreamonhglobal​docs.microsoft.com

当我们了解每一个接口的含义之后,我们就可以这样来保存/读取一个可持久化对象(假设IMessage类是一个可持久化类,并且我们实现了其可持久化接口):

int main()
{HRESULT hr = S_OK;hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);if (SUCCEEDED(hr)){// 创建一个IMessageCComPtr<IMessage> pMsg;hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_INPROC_SERVER, IID_IMessage, (void**)&pMsg);// 获取其持久化接口CComPtr<IPersistStream> pPersist;pPersist = pMsg;// 创建一个流CComPtr<IStream> pStream;CreateStreamOnHGlobal(NULL, TRUE, &pStream);// 保存流pPersist->Save(pStream, TRUE);// 重置流LARGE_INTEGER pos;pos.QuadPart = 0;pStream->Seek(pos, STREAM_SEEK_SET, NULL);// 创建另外一个对象CComPtr<IMessage> pAnother;hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_INPROC_SERVER, IID_IMessage, (void**)&pAnother);// 从流中还原此对象CComPtr<IPersistStream> pAnotherStream;pAnotherStream = pAnother;pAnotherStream->Load(pStream);}CoUninitialize();return 0;
}

注意标黑的创建另外一个对象部分,在这个例子中,我们已经明确了我们要创建的对象是IMessage,但是更多的情况是,我们并不知道自己要创建什么对象,我们很可能只是从硬盘上获取了一个流,然后我们需要创建它,并且调用IPersistStream::Load。然而我们并不知道它的CLSID,因此无法获取CoCreateInstance的第一个参数。所以,我们在写入一个流时,我们先要通过IPersist::GetClassID获取其ID,然后先写入流中。在创建的时候,我们先读取流中的CLSID,然后调用CoCreateInstance,最终才调用IPersistStream::Load,这样便可以实现通过流来创建任何可持久化的对象。幸运的是,COM对这样的流程提供了支持,接下来我们实现自己的IPersistStream(Init)。

三、实现IPersistStreamInit

接下来我们来实现IPersistStreamInit接口,使得一个类能够持久化。

首先我们写一个简单的类,继承了一个IMessage接口,里面有一个Print函数,简单地把自己的一个成员打印出来:

// idl
import "oaidl.idl";
import "ocidl.idl";[uuid(dfa98c1f-2f81-4115-8dd5-692d91ee7342),version(1.0),
]
library ATLProject1Lib
{importlib("stdole2.tlb");[object, uuid(8B82ACF5-2A77-4385-B254-4BC5C6F12FB5)]interface IMessage : IUnknown{HRESULT Print();};[uuid(B8922344-0FD5-4630-A4D7-DD9C9321BBB1)]coclass Message{interface IMessage;};
};import "shobjidl.idl";// 头文件
#include <atlcom.h>
#include "ATLProject1_i.h"
using namespace ATL;class MessageImpl: public CComObjectRoot, public CComCoClass<MessageImpl, &CLSID_Message>, public IMessage, public IPersistStreamInit, public IPersistStream
{BEGIN_COM_MAP(MessageImpl)COM_INTERFACE_ENTRY(IMessage)COM_INTERFACE_ENTRY(IPersistStreamInit)COM_INTERFACE_ENTRY(IPersistStream)END_COM_MAP()MessageImpl();~MessageImpl();public:DECLARE_REGISTRY_RESOURCEID(IDR_ATLPROJECT1);STDMETHODIMP Print() override;STDMETHODIMP Set(int) override;private:// 通过 IPersistStreamInit 继承virtual HRESULT __stdcall GetClassID(CLSID* pClassID) override;virtual HRESULT __stdcall IsDirty(void) override;virtual HRESULT __stdcall Load(LPSTREAM pStm) override;virtual HRESULT __stdcall Save(LPSTREAM pStm, BOOL fClearDirty) override;virtual HRESULT __stdcall GetSizeMax(ULARGE_INTEGER* pCbSize) override;virtual HRESULT __stdcall InitNew(void) override;private:int m_number;bool m_dirty;
};OBJECT_ENTRY_AUTO(CLSID_Message, MessageImpl);

构造函数、析构函数、Print、Set实现如下:

MessageImpl::MessageImpl(): m_number(0), m_dirty(false)
{}MessageImpl::~MessageImpl()
{}STDMETHODIMP_(HRESULT __stdcall) MessageImpl::Print()
{std::cout << m_number << std::endl;return S_OK;
}STDMETHODIMP MessageImpl::Set(int n)
{if (m_number != n){m_number = n;m_dirty = true;}
}

接下来就是重点,如何实现IPersistStreamInit:

HRESULT __stdcall MessageImpl::GetClassID(CLSID* pClassID)
{if (!pClassID)return E_POINTER;*pClassID = CLSID_Message;return S_OK;
}HRESULT __stdcall MessageImpl::IsDirty(void)
{return m_dirty ? S_OK : S_FALSE;
}HRESULT __stdcall MessageImpl::GetSizeMax(ULARGE_INTEGER* pCbSize)
{if (!pCbSize)return E_POINTER;pCbSize->QuadPart = sizeof(m_number);return S_OK;
}HRESULT __stdcall MessageImpl::InitNew(void)
{m_number = 0;return S_OK;
}

我们先看如何实现几个简单的函数:GetClassID直接返回了自己的CLSID,IsDirty返回当前类的状态是否被修改,InitNew我们把成员置位0。

下面开始实现最重要的Save和Load方法:

HRESULT __stdcall MessageImpl::Save(LPSTREAM pStm, BOOL fClearDirty)
{CLSID clsid;GetClassID(&clsid);/* 写CLSID */pStm->Write(&clsid, sizeof(CLSID), NULL);/* 写数据 */pStm->Write(&m_number, sizeof(m_number), NULL);if (fClearDirty)m_dirty = false;return S_OK;
}HRESULT __stdcall MessageImpl::Load(LPSTREAM pStm)
{/* 读取数据 */pStm->Read(&m_number, sizeof(int), NULL);return S_OK;
}

这里有个重要的规则:我们在Save中先会将CLSID写入流,然后再写自己的成员。而在Load的时候,我们只直接读取成员,而读取CLSID的部分,由用户来调用。由此我们可以写出下面的测试用例:

int main()
{HRESULT hr = S_OK;hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);if (SUCCEEDED(hr)){// 创建一个IMessageCComPtr<IMessage> pMsg;hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_INPROC_SERVER, IID_IMessage, (void**)&pMsg);// 设置一个初值10pMsg->Set(10);// 获取其持久化接口CComPtr<IPersistStreamInit> pPersist;pPersist = pMsg;// 创建一个流CComPtr<IStream> pStream;CreateStreamOnHGlobal(NULL, TRUE, &pStream);// 保存流pPersist->Save(pStream, TRUE);// 重置流LARGE_INTEGER pos;pos.QuadPart = 0;pStream->Seek(pos, STREAM_SEEK_SET, NULL);// 创建另外一个对象CLSID clsid;pStream->Read(&clsid, sizeof(clsid), NULL);CComPtr<IUnknown> pAnother;hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pAnother);// 从流中还原此对象CComPtr<IPersistStreamInit> pAnotherStream;pAnotherStream = pAnother;pAnotherStream->InitNew();pAnotherStream->Load(pStream);// 打印对象CComPtr<IMessage> pAnotherMsg;pAnotherMsg = pAnotherStream;pAnotherMsg->Print();}CoUninitialize();return 0;
}

注意到黑体部分,我们假设并不知道它是从CLSID_Message中创建的对象,但是我们知道它先写了一个CLSID到Stream,所以我们先会读取16个字节来拿到CLSID,然后通过CoCreateInstance创建一个IUnknown对象,接着调用IPersistStreamInit的InitNew和Load来加载数据。

我们最终将它转换为IMessage来验证我们的流程是否正确。最终打印的结果确实是10,说明流程无误。

四、优化流程

在上面的例子中,很重要的一点是,我们自己约定了CLSID最开始是通过Save写入了IStream,所以我们在Load之前需要先读取CLSID个字节,然后再来创建对象。

这样的约定,非常不靠谱,因为客户并不一定知道在Load之前需要读取多少个字节。 而我们创建对象的流程基本一致:先从流中读取CLSID,接着调用CoCreateInstance,然后调用InitNew以及Load。对象的写入流程基本也一致,先写CLSID,接着再写自己的内容。

微软提供了一对函数来实现写CLSID和读取CLSID:

WriteClassStmReadClassStm:

https://docs.microsoft.com/zh-cn/windows/win32/api/coml2api/nf-coml2api-writeclassstm​docs.microsoft.comReadClassStm function (coml2api.h) - Win32 apps​docs.microsoft.com

它们做的事情,就类似我们上面例子中,将CLSID写入IStream中。

所以Save函数可以变为这样:

HRESULT __stdcall MessageImpl::Save(LPSTREAM pStm, BOOL fClearDirty)
{CLSID clsid;GetClassID(&clsid);/* 写CLSID */WriteClassStm(pStm, clsid);/* 写数据 */pStm->Write(&m_number, sizeof(m_number), NULL);if (fClearDirty)m_dirty = false;return S_OK;
}

测试用例黑体部分,变为这样:

// 创建另外一个对象
CLSID clsid;
ReadClassStm(pStream, &clsid);CComPtr<IUnknown> pAnother;
hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pAnother);

不过依照MSDN文档,微软另外封装了一对函数,来完成整个对象的创建和写入:

OleLoadFromStreamOleSaveToStream

OleLoadFromStream function (ole.h) - Win32 apps​docs.microsoft.com

OleSaveToStream function (ole2.h) - Win32 apps​docs.microsoft.com

OleLoadFromStream首先是调用ReadClassStm获取对象CLSID,然后调用CoCreateInstance创建对象,最后调用对象的IPersistStream的Load接口。

OleSaveToStream首先调用IPersist::GetClassID获取CLSID,然后调用WriteClassStm,最后调用IPersistStream的Save接口。

由于读写CLSID都在类的外部,所以我们实现类的时候,Load和Save就不需要对其CLSID进行处理了。那么,我们的Load和Save函数可以变成这样:

// 只处理自己的数据
HRESULT __stdcall MessageImpl::Save(LPSTREAM pStm, BOOL fClearDirty)
{/* 写数据 */pStm->Write(&m_number, sizeof(m_number), NULL);if (fClearDirty)m_dirty = false;return S_OK;
}HRESULT __stdcall MessageImpl::Load(LPSTREAM pStm)
{/* 读取数据 */pStm->Read(&m_number, sizeof(int), NULL);return S_OK;
}

测试用例做对应的修改:

int main()
{HRESULT hr = S_OK;hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);if (SUCCEEDED(hr)){// 创建一个IMessageCComPtr<IMessage> pMsg;hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_INPROC_SERVER, IID_IMessage, (void**)&pMsg);// 设置一个初值10pMsg->Set(10);// 获取其持久化接口CComPtr<IPersistStreamInit> pPersist;pPersist = pMsg;// 创建一个流CComPtr<IStream> pStream;CreateStreamOnHGlobal(NULL, TRUE, &pStream);// 保存流CComPtr<IPersistStream> pPersistStream;pPersistStream = pPersist;OleSaveToStream(pPersistStream, pStream);// 重置流LARGE_INTEGER pos;pos.QuadPart = 0;pStream->Seek(pos, STREAM_SEEK_SET, NULL);// 创建另外一个对象CComPtr<IUnknown> pAnother;OleLoadFromStream(pStream, IID_IUnknown, (void**)&pAnother);// 打印对象CComPtr<IMessage> pAnotherMsg;pAnotherMsg = pAnother;pAnotherMsg->Print();}CoUninitialize();return 0;
}

OleSaveToStream只接受IPersistStream接口,所以我们稍微转换一下。我们通过OleSaveToStream保存了对象的状态,并且通过OleLoadFromStream又创建了它的一个克隆,这样整个代码都显得非常简洁。

五、基于属性的持久化 IPersistPropertyBag

一些高级语言,例如Visual Basic、Java等,提供了属性的关键字。所谓属性集合,其实就是一个对象总的一个Map。属性的实现可以理解为操作一个std::map<std::wstring, VARIANT>,COM提供了IPersistPropertyBag来持久化属性。

interface IPersistPropertyBag : IPersist
{HRESULT InitNew(void);HRESULT Load([in] IPropertyBag* pPropBag, [in] IErrorLog* pErrorLog);HRESULT Save([in] IPropertyBag* pPropBag, [in] BOOL fClearDirty, [in] BOOL fSaveAllProperties);
}

我们可以看到熟悉的InitNew, Load和Save,不过它的入参是IPropertyBag而不是IStream。IPropertyBag定义如下:

interface IPropertyBag : IUnknown
{// 获取某个名字为pszPropName的属性HRESULT Read([in] LPCOLESTR pszPropName, [in, out] VARIANT* pVar, [in] IErrorLog* pErrorLog);// 写入某个名字为pszPropName的属性HRESULT Write([in] LPCOLESTR pszPropName, [in] VARIANT* pVar);
}

这个其实就是基于键值操作的一套接口,概念非常简单。

在VB中,如果一个类标记为了Persistable,那么VB会为这个类自动实现IPersistIPersistStreamIPersistStreamInit以及IPersistPropertyBag

对于一个VB的类:

' 类似 IPersistPropertyBag::InitNew
Private Sub Class_InitProperties()
End Sub' 类似 IPersistPropertyBag::Load
Private Sub Class_ReadProperties(PropBag As PropertyBag)m_x = PropBag.ReadProperty("x", 0)
End Sub' 类似 IPersistPropertyBag::Save
Private Sub Class_WriteProperties(PropBag As PropertyBag)PropBag.WriteProperty "x", m_x
End Sub

PropBag.ReadProperty和PropBag.WriteProperty分别会调用IPropertyBag::Read和IPropertyBag::Write。

六、结构化存储

上面讲到的IPersistStream(Init)接口是基于流(二进制)的,我们需要自己定义对象的二进制结构,然后决定如何写入流。COM提供一种处理结构化对象的接口叫做IStorage,所谓结构化,可以理解为像文件目录那样,存在节点、键值对等,我们多年前使用的doc、xls文档,都属于这种结构化的文档。

为了理解结构化存储,请将它就想象成一个文件系统结构,IStorage中提供的操作,则是增加、删除文件、增加文件夹、移动文件夹、删除文件夹等。

// 将IStorage想象为一个文件夹系统
interface IStorage : IUnknown
{// 在此对象中创建流,名字为pwcsName(类似于创建一个文件)HRESULT CreateStream([in, string] const OLECHAR *pwcsName,[in] DWORD grfMode,[in] DWORD reserved1,[in] DWORD reserved2,[out] IStream **ppstm);// 在此对象中打开一个名字为pwcsName的流(类似于打开一个文件)[local]HRESULT OpenStream([in, string] const OLECHAR *pwcsName,[in, unique] void *reserved1,[in] DWORD grfMode,[in] DWORD reserved2,[out] IStream **ppstm);// 在此对象下再创建一个IStorage对象,名字为pwcsName(类似于创建一个文件夹)HRESULT CreateStorage([in, string] const OLECHAR *pwcsName,[in] DWORD grfMode,[in] DWORD reserved1,[in] DWORD reserved2,[out] IStorage **ppstg);// 在此对象中打开一个名为pwcsName的IStorage(类似于打开一个文件夹)HRESULT OpenStorage([in, unique, string] const OLECHAR *pwcsName,[in, unique] IStorage *pstgPriority,[in] DWORD grfMode,[in, unique] SNB snbExclude,[in] DWORD reserved,[out] IStorage **ppstg);// 拷贝整个对象到另外一个IStorage(类似于拷贝文件夹)[local]HRESULT CopyTo([in] DWORD ciidExclude,[in, unique, size_is(ciidExclude)] IID const *rgiidExclude,[in, unique, annotation("__RPC__in_opt")] SNB snbExclude,[in, unique] IStorage *pstgDest);// 移动子IStorage对象(类似于移动子文件夹)HRESULT MoveElementTo([in, string] const OLECHAR * pwcsName,[in, unique] IStorage *pstgDest,[in, string] const OLECHAR *pwcsNewName,[in] DWORD grfFlags);// 提交事务HRESULT Commit([in] DWORD grfCommitFlags);// 回滚事务HRESULT Revert();// 枚举所有元素(类似于列举文件夹下所有内容)[local]HRESULT EnumElements([in] DWORD reserved1,[in, unique, size_is(1)] void *reserved2,[in] DWORD reserved3,[out] IEnumSTATSTG **ppenum);// 删除名字为pwcsName的对象(类似于删除文件或文件夹)HRESULT DestroyElement([in, string] const OLECHAR *pwcsName);// 重命名元素(类似于重命名文件或文件夹)HRESULT RenameElement([in, string] const OLECHAR *pwcsOldName,[in, string] const OLECHAR *pwcsNewName);// 设置创建、修改、访问时间属性HRESULT SetElementTimes([in, unique, string] const OLECHAR *pwcsName,[in, unique] FILETIME const *pctime,[in, unique] FILETIME const *patime,[in, unique] FILETIME const *pmtime);// 设置此对象的CLSIDHRESULT SetClass([in] REFCLSID clsid);// 设置额外信息HRESULT SetStateBits([in] DWORD grfStateBits,[in] DWORD grfMask);// 获取对象信息(类似于获取文件夹信息)HRESULT Stat([out] STATSTG *pstatstg,[in] DWORD grfStatFlag);
}

可见,此对象像是对结构化目录量身定制的,它常常也被使用在嵌入型文档中,例如:一个doc文档中嵌入了一个bmp,一个xls,那么它便可以提供这个接口来检索。接口要么可以读写一个流,要么可以读写一个IStorage,这是一个非常典型的树状结构了。

我们不会花大力起来实现一个IStorage,因为它和具体业务相关。微软提供了一对函数来读取结构化文件:StgCreateStorageEx和StgOpenStorageEx:

https://docs.microsoft.com/en-us/windows/win32/api/coml2api/nf-coml2api-stgcreatestorageex​docs.microsoft.comStgOpenStorageEx function (coml2api.h) - Win32 apps​docs.microsoft.com

它创建了微软自己实现的一种结构化文件,我们可以叫它stg文件。下面是测试其读写的代码:

int main()
{HRESULT hr = S_OK;{/* 创建一个stg文件 */CComPtr<IStorage> pStorage;hr = StgCreateStorageEx(L"D:test.stg",STGM_DIRECT | STGM_CREATE | STGM_READWRITE | STGM_SHARE_EXCLUSIVE,STGFMT_STORAGE, 0, 0, 0, IID_IStorage, (void**)&pStorage);/* 创建一个叫MyFile的流结点 */CComPtr<IStream> pStream;pStorage->CreateStream(OLESTR("MyFile"), STGM_DIRECT | STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,0, 0, &pStream);/* 写入数据 */char str[] = "hello world";size_t sz = sizeof(str);pStream->Write(&sz, sizeof(size_t), NULL);pStream->Write(str, sz, NULL);}{/* 测试读取 */CComPtr<IStorage> pStorage;hr = StgOpenStorageEx(L"D:test.stg",STGM_DIRECT | STGM_READWRITE | STGM_SHARE_EXCLUSIVE,STGFMT_STORAGE, 0, 0, 0, IID_IStorage, (void**)&pStorage);/* 获取MyFile流结点 */CComPtr<IStream> pStream;hr = pStorage->OpenStream(OLESTR("MyFile"), NULL, STGM_DIRECT | STGM_READ | STGM_SHARE_EXCLUSIVE,0, &pStream);/* 读取数据 */size_t sz;pStream->Read(&sz, sizeof(size_t), NULL);char* buffer = (char*)_alloca(sz);pStream->Read(buffer, sz, NULL);printf("%s", buffer);}return 0;
}

我们先创建了一个stg文件,然后在里面创建了个叫MyFile的流。我们把一个字符串的大小和内容依次写入流。接下来我们打开了这个stg文件,读取它之前写入的字符串大小,然后为buffer分配栈空间,再将字符串内容读取到buffer中。我们通过printf来验证buffer是否正确,程序运行后输出了hello world,说明整个流程无误。

七、基于属性的结构化存储

我们在系统中右键文件,会出现一个「摘要」信息,里面正是属性对,由键-值组成。我们可以通过COM接口,来对文件的属性进行修改。COM提供了IPropertyStorage和IPropertySetStorage来获取和修改文档的属性。

IPropertySetStorage类似于IPresistPropertyBag,可以获取属性集合,也就是拿到IPropertyStorage,而IPropertyStorage类似于IPropertyBag,可以操作属性。

例如,我们需要在test.stg中加上属性:标题=My Title,那么代码如下:

int main()
{HRESULT hr = S_OK;{/* 创建一个stg文件 */CComPtr<IStorage> pStorage;hr = StgCreateStorageEx(L"D:test.stg",STGM_DIRECT | STGM_CREATE | STGM_READWRITE | STGM_SHARE_EXCLUSIVE,STGFMT_STORAGE, 0, 0, 0, IID_IStorage, (void**)&pStorage);/* 创建一个叫MyFile的流结点 */CComPtr<IStream> pStream;pStorage->CreateStream(OLESTR("MyFile"), STGM_DIRECT | STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,0, 0, &pStream);/* 写入数据 */char str[] = "hello world";size_t sz = sizeof(str);pStream->Write(&sz, sizeof(size_t), NULL);pStream->Write(str, sz, NULL);/* 获取属性集 */CComPtr<IPropertySetStorage> pPropertySetStorage;pPropertySetStorage = pStorage;/* 打开Summary属性 */CComPtr<IPropertyStorage> pPropertyStorage;hr = pPropertySetStorage->Create(FMTID_SummaryInformation, NULL, PROPSETFLAG_DEFAULT,STGM_CREATE | STGM_READWRITE | STGM_SHARE_EXCLUSIVE,&pPropertyStorage);/* 写入属性 */PROPSPEC ps;ps.ulKind = PRSPEC_PROPID;ps.propid = PIDSI_TITLE;PROPVARIANT pv;pv.vt = VT_LPSTR;char szTitle[] = "My Title";pv.pszVal = szTitle;hr = pPropertyStorage->WriteMultiple(1, &ps, &pv, PID_FIRST_USABLE);}//...return 0;
}

黑色的部分,是我们相对之前的demo新增的写属性的部分。首先我们从IStorage接口中拿出一个IPropertySetStorage,COM中将这个IStorage的实例继承了IPropertySetStorage。接着它创建了一个FMTID_SummaryInformation的IPropertyStorage,这个FMTID_SummaryInformation是微软预定义的,表示「摘要」。最后我们用IPropertyStorage创建了一个类型为PIDSI_TITLE(表示标题),值为My Title的属性。

个人认为,结构化存储是微软的一个美好愿望,但是实际上要不要用,这个还是要自己再考虑一下。里面很多接口是基于现成实现的,所以才会留下很多含糊不清的reserved字段。如果想进一步了解结构化存储,可以点入下面的链接:

Structured Storage - Win32 apps​docs.microsoft.com

下一篇:

Froser:COM编程攻略(十六 名字对象IMoniker与对象运行表ROT)​zhuanlan.zhihu.com

结构 win32_COM编程攻略(十五 持久化与结构化存储)相关推荐

  1. c6011取消对null指针的引用_COM编程攻略(二十二 IDL中的枚举,指针,数组)

    上一篇: Froser:COM编程攻略(二十一 异步)​zhuanlan.zhihu.com 本篇主要讲idl的一些语法特性. idl的语法和C语言非常类似,但是它扩展了一些特性,这些特性用于兼容其它 ...

  2. opencv3计算机视觉python语言实现pdf_对比《OpenCV计算机视觉编程攻略第3版》《OpenCV 3计算机视觉Python语言实现第2版》PDF代码......

    OpenCV 3是一种先进的计算机视觉库,可以用于各种图像和视频处理操作,通过OpenCV 3 能很容易地实现一些有前景且功能先进的应用(比如:人脸识别或目标跟踪等).从图像处理的基本操作出发,计算机 ...

  3. C语言编程>第二十五周 ② 下列程序中,函数fun的功能是:将大写字母转换为对应小写字母之后的第五个字母,若为小写字母为v~z,使小写字母的值减21,转换后的小写字母作为函数值返回。

    例题:下列程序中,函数fun的功能是:将大写字母转换为对应小写字母之后的第五个字母,若为小写字母为v-z,使小写字母的值减21,转换后的小写字母作为函数值返回. 例如,若形参是字母A,则转换为小写字母 ...

  4. C语言编程>第二十五周 ⑤ 下列给定程序的功能是:读入一个英文文本行,将其中每个单词的第一个字母改成大写,然后输出此文本行(这里的 “单词”是指由空格隔开的字符串)。

    例题:下列给定程序的功能是:读入一个英文文本行,将其中每个单词的第一个字母改成大写,然后输出此文本行(这里的 "单词"是指由空格隔开的字符串). 例如,若输入 "good ...

  5. C语言编程>第二十五周 ③ 下列给定程序中,函数fun的功能是:根据输入的三个边长(整型值),判断能否构成三角形;构成的是等边三角形,还是等腰三角形。若能构成等边三角形函数返回3,若能构成……

    例题:下列给定程序中,函数fun的功能是:根据输入的三个边长(整型值),判断能否构成三角形:构成的是等边三角形,还是等腰三角形.若能构成等边三角形函数返回3,若能构成等腰三角形函数返回2,若能构成三角 ...

  6. OpenCV计算机视觉编程攻略之行人检测

    OpenCV计算机视觉编程攻略之行人检测,OpenCV 提供了一个基于HOG 和SVM且经过训练的行人检测器,可以用这个SVM 分类器以不同尺度的窗口扫描图像,在完整的图像中检测特定物体. 原图如下: ...

  7. OpenCV计算机视觉编程攻略之提取图片轮廓-使用Canny函数

    OpenCV计算机视觉编程攻略之提取图片轮廓-使用Canny函数,很方便..代码如下: #include <vector> #include <iostream> #inclu ...

  8. OpenCV计算机视觉编程攻略之生成椒盐噪声实现

    OpenCV计算机视觉编程攻略(第3版)P21的访问像素值,生成椒盐噪声实现. 运行结果图片,截图如下: 看书留下记录,代码如下: #include <random> #include & ...

  9. python 教程 第十五章、 结构布局

    第十五章. 结构布局 #!/usr/bin/env python #(1)起始行 "this is a module" #(2)模块文档 import sys #(3)模块导入 d ...

最新文章

  1. 边工作边刷题:70天一遍leetcode: day 27
  2. ASP.NET程序中常用的三十三种代码(转载)
  3. SD_CUSTOMER_MAINTAIN_ALL
  4. python中split_python中split()和split(' ')的区别
  5. vue实现首屏加载等待动画 避免首次加载白屏尴尬
  6. renew process 更新过程
  7. ECM之ucf session wait timeout【DFC_ACS_LOG_NO_NL】问题分析
  8. 2016学计算机软件,2016年夏季学期计算机(软件)学院学年论文字数、页数和格式要求.doc...
  9. logstash过滤器插件filter详解及实例
  10. Google推出免费公共域名解析DNS服务
  11. 清空表与删除表mysql
  12. 190403 联众验证码 - python3接入
  13. 小D课堂-jekins-01
  14. 各种音视频编解码学习详解之 编解码学习笔记(七):微软Windows Media系列
  15. PHP实训笔记,Java实训笔记(八)之mysql
  16. C语言和设计模式-工厂方法
  17. [MFC] CList
  18. 怎么把pdf格式转成word文档?如何将 PDF 转换为 Word
  19. 什么是好的录屏软件?5 款值得收藏的屏幕录制软件
  20. Tomcat Servlet Request

热门文章

  1. 我的简书两月记:数据可视化
  2. 【译】用Fragment创建动态的界面布局(附Android示例代码)
  3. HttpServletRequest和HttpServletResponse简介
  4. column 'XXXX' in field list is ambiguous
  5. 标准博客 API .BLOG APIS
  6. linux 禁用 ctrl+alt+del 重启系统
  7. java jar 和 war 包的区别
  8. python3 list 列表 方法说明
  9. linux sed命令 常用方法
  10. shell 实现ip字符串与整形互转