前情提要:
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.comIStream继承于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-createstreamonhglobaldocs.microsoft.com当我们了解每一个接口的含义之后,我们就可以这样来保存/读取一个可持久化对象(假设IMessage类是一个可持久化类,并且我们实现了其可持久化接口):
int main()
{
HRESULT hr = S_OK;
hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if (SUCCEEDED(hr))
{
// 创建一个IMessage
CComPtr<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))
{
// 创建一个IMessage
CComPtr<IMessage> pMsg;
hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_INPROC_SERVER, IID_IMessage, (void**)&pMsg);
// 设置一个初值10
pMsg->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:
WriteClassStm和ReadClassStm:
https://docs.microsoft.com/zh-cn/windows/win32/api/coml2api/nf-coml2api-writeclassstmdocs.microsoft.com ReadClassStm function (coml2api.h) - Win32 appsdocs.microsoft.com![64331dea9acd5da35eb84bb45b4649f0.png](https://i-blog.csdnimg.cn/blog_migrate/da8f06bbdd01e92f583203d76baa7e91.png)
它们做的事情,就类似我们上面例子中,将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文档,微软另外封装了一对函数,来完成整个对象的创建和写入:
OleLoadFromStream和OleSaveToStream:
OleLoadFromStream function (ole.h) - Win32 appsdocs.microsoft.com![64331dea9acd5da35eb84bb45b4649f0.png](https://i-blog.csdnimg.cn/blog_migrate/da8f06bbdd01e92f583203d76baa7e91.png)
![64331dea9acd5da35eb84bb45b4649f0.png](https://i-blog.csdnimg.cn/blog_migrate/da8f06bbdd01e92f583203d76baa7e91.png)
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))
{
// 创建一个IMessage
CComPtr<IMessage> pMsg;
hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_INPROC_SERVER, IID_IMessage, (void**)&pMsg);
// 设置一个初值10
pMsg->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会为这个类自动实现IPersist、IPersistStream、IPersistStreamInit以及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);
// 设置此对象的CLSID
HRESULT 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-stgcreatestorageexdocs.microsoft.com StgOpenStorageEx function (coml2api.h) - Win32 appsdocs.microsoft.com![64331dea9acd5da35eb84bb45b4649f0.png](https://i-blog.csdnimg.cn/blog_migrate/da8f06bbdd01e92f583203d76baa7e91.png)
它创建了微软自己实现的一种结构化文件,我们可以叫它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,说明整个流程无误。
七、基于属性的结构化存储
![3a65306fa59c2bc648aefe0141449a00.png](https://i-blog.csdnimg.cn/blog_migrate/c67a15d243e137ecbd3da0dbf610e4f9.png)
我们在系统中右键文件,会出现一个「摘要」信息,里面正是属性对,由键-值组成。我们可以通过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 appsdocs.microsoft.com![64331dea9acd5da35eb84bb45b4649f0.png](https://i-blog.csdnimg.cn/blog_migrate/da8f06bbdd01e92f583203d76baa7e91.png)
下一篇:
Froser:COM编程攻略(十六 名字对象IMoniker与对象运行表ROT)zhuanlan.zhihu.com