windows 编程 edit 写入_COM编程攻略(十五 持久化与结构化存储)

前情提要:

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))
	{
		// 创建一个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:

WriteClassStmReadClassStm:

https://docs.microsoft.com/zh-cn/windows/win32/api/coml2api/nf-coml2api-writeclassstm​docs.microsoft.com ReadClassStm function (coml2api.h) - Win32 apps​docs.microsoft.com
64331dea9acd5da35eb84bb45b4649f0.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文档,微软另外封装了一对函数,来完成整个对象的创建和写入:

OleLoadFromStreamOleSaveToStream

OleLoadFromStream function (ole.h) - Win32 apps​docs.microsoft.com
64331dea9acd5da35eb84bb45b4649f0.png
OleSaveToStream function (ole2.h) - Win32 apps​docs.microsoft.com
64331dea9acd5da35eb84bb45b4649f0.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会为这个类自动实现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);

    // 设置此对象的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-stgcreatestorageex​docs.microsoft.com StgOpenStorageEx function (coml2api.h) - Win32 apps​docs.microsoft.com
64331dea9acd5da35eb84bb45b4649f0.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

我们在系统中右键文件,会出现一个「摘要」信息,里面正是属性对,由键-值组成。我们可以通过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
64331dea9acd5da35eb84bb45b4649f0.png

下一篇:

Froser:COM编程攻略(十六 名字对象IMoniker与对象运行表ROT)​zhuanlan.zhihu.com
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值