限于一直使用微软的richedit控件很多东西有问题都摸不着头脑,而工程使用中其实需要用到的并不高级需求也不高,需求就是快速简单的实现一个图文并茂的富文本显示工具;为此 ,我开始尝试设计一个简单易用的富文本显示和编辑工具,现在初步核心框架已经完成,为此分享给各位我的设计思路,欢迎拍砖~.~。 源代码在文章的结束处可下载。
首先,我们开始思考富文本控件特点:多行、图文并茂、样式自定义、光标命中、可拷贝、可黏贴、文本格式化(换行、缩进,这部分暂无考虑,因为这部分可以通过简易的 编辑实现);此时,我们开始思考这个富文本控件的整体框架,从我们列出的需求中进行核心数据结构抽取和搭建:第一,必须有一存放字符串的容器(data);第二, 样式自定义,那么就需要一个style容器(stylelist),图片其实也是样式的一种,那么我们就把它归类为style;第三,文本格式化,最重要的就是换行,那么我们就需要一个专门 来管理每一行状态的类;完成了基本的核心数据结构的设计以后,下一步我们要做的就是对每一种核心数据都需要有一个专门的类进行管理。
当前我们已经设计好了核心数据结构,让我们从基本的零部件设计开始做起:
一,字符串管理,我们需要的只是简单插入、删除、获取功能,我借鉴nopad++开源项目中的底层数组类SplitVector,是一个性能相当好的数组管理类,里面已经集成我们所需要的几个 基本功能,所以我就直接用它来存放数据;我们把一个char看成是此类中最小的数据单元;
二,样式管理,同样我们需要数组的基本功能,但是我们对样式的管理需求更复杂一点,所以我们需要专门创建一个管理类,里面包含一个样式数组,我们需要有样式的position定位查找功能等, 比如光标命中和绘画,我们暂且想到这些,为其封装一个行为集合类;我们把每一个style看成为最小数据单元,并定义出基本属性:
struct StyleObj
{
enum StyleType
{
ST_CHAR = 0,
ST_OBJ
};
enum StyleRemoveType
{
SRT_SUCCEED = 0,
SRT_ALLDELETE,
SRT_NOINRANGE
};
int type_;//style樣式,字符串樣式或者obj樣式
int charPos_;//樣式在字符串中起點位置
int charLength_;//樣式覆蓋字符串長度
CharFormat cf_;//樣式
IObj* obj_;//擴展對象
LinkObj* link_;//鏈接對象
HFONT font_;//樣式對應字體
int nRef_;//引用計數器
};
鉴于篇幅,我在此只列出我们需要用到的基本属性;三,行管理,行与样式一样,需要的功能集合差不多,所以我们单独给他封装一个管理类;我们把每一个line看成为最小数据单元,并定义出基本属性:
struct LineObj : public IBaseObj
{
enum PosType{
low = 0,
in,
beyond
};
int beginPos_;//行在字符串中起點位置
int endPos_;//行在字符串中終點位置
int width_;//行寬
int lineIndex_;//行索引
RowObjDetail Details;//行詳細屬性,最高高度,最高高度位置
int indent_;//for paragraph,段落
int yPos_;//行頂部在視口Y軸座標
int nRef_;//對象引用計數
};
所有涉及到的辅组数据结构我将在文章结束的时候附上;好了,走到这一步我们已经把基本的核心数据结构做好了,我们下一步要做的就是核心数据处理整合管理,我们所有的逻辑将是围绕这三个数据结构扩展和搭建的。 现在我们开始做的就是整体控件框架的搭建,从最外层呈现看这个控件可以分为以下几个部分:消息处理模块、核心数据操作模块、绘画模块;这其实就是一个控件的基本逻辑,收到 消息=》处理数据=》更新设备DC。
显然,我们必须要独立开控件的三个模块的东西,消息处理模块与系统打交道,它相当于一个外壳,得到外界的指令后将其传给我们的数据处理中心,即是核心数据操作模块, 核心数据处理模块处理完以后再消息通知外壳,外壳将绘画指令传给绘画模块,绘画模块根据数据中心数据属性进行相对于的呈现丢到外壳上面去。 分析完这三个模块,我们开始模块类封装:
/*消息處理類,負責最外層收到消息後的分配和相關響應處理*/
class RichEditor
{
public:
RichEditor();
protected:
bool bTraceMouse_;
bool bLBButtonDown_;
Selection selectRange_;
Document document_;
DocumentDetails details_;
Window window_;
CHARFORMAT cfDefault_;
CaretPosition caretPosition_;
};
/*文檔操作類,負責文檔的插入刪除,更新,查找等功能*/
class Document
{
public:
Document();
protected:
SplitVector<MY_CHAR> data;//if is obj, use the '%' as symbol
StyleObjManager styleList_;
LineObjManager LineObjList_;
DocumentDetails* details_;
int ident_;
Window* window_;
Selection* selectionRange_;
};
/*窗口類,負責最終繪畫和長度計算,窗口基本屬性存儲*/
class Window
{
public:
Window();
~Window();
void Attach(HWND hWnd);
void Detch();
int GetCharLenght(MY_CHAR c, CHARFORMAT& cf, HFONT font);
int GetTextLenght(MY_CHAR* c, int length, CHARFORMAT& cf, HFONT font);
void PaintText(MY_CHAR* c, int length, RECT& rcText, CHARFORMAT& cf, HDC hdcPaint, HFONT font);
void PaintObj(IObj* obj, RECT& rcObj, HDC hdcPaint, bool bSelect = false);
void ReDraw();
HWND GetWnd();
protected:
HWND hWnd_;
CHARFORMAT cfCur_;
bool bSeletionMode_;
bool bDoubleBuffering_;
HBRUSH m_brushFrame;
HBRUSH m_brushSelect;
};
到此,我们控件的基本框架已经搭建完成,而下一步要做的就是实现核心逻辑,就是插入删除一个字符串或者插入一个扩展obj要走的流程,在这里我就不一一把代码放出来 ,限于代码篇幅,我只提供下思路哈:
插入:以'\n'为分隔符分别插入,style更新,data更新,line更新
void Document::InsertString(const MY_CHAR* c, int pos, int length, CHARFORMAT& cf, MY_CHAR* hyperLinkData)
{
int begin = 0;
int begin1 = 0;
int end = length;
const MY_CHAR* cTemp = c;
const MY_CHAR* cTemp1 = c;
styleList_.Log();
styleList_.InsertStyle(pos, length, cf, hyperLinkData);
styleList_.Log();
while(begin1 < end)
{
if (*cTemp == '\n')
{
int insertLenght = begin1-begin;
data.InsertFromArray(pos, cTemp1, 0, insertLenght);
UpdateLineManager_insert(pos, insertLenght);
pos += begin1;
begin = begin1;
cTemp1 = cTemp;
}
cTemp++;
begin1++;
}
if (begin < begin1)
{
int insertLenght = begin1-begin;
data.InsertFromArray(pos, cTemp1, 0, insertLenght);
UpdateLineManager_insert(pos, insertLenght);
pos += begin1;
begin = begin1;
cTemp1 = cTemp;
}
ReDraw();
}
void Document::DeleteRange(int begin, int end)
{
data.DeleteRange(begin, end - begin + 1);
Log();
UpdateStyleManager_delete(begin, end);
UpdateLineManager_delete(begin, end);
}
这样,一个完整的富文本控件就被我们拆分成一个一个小块的零部件,然后又将每个零部件组装后合成一个完成的控件类,当然里面会用到的拷贝黏贴需要用到IDataObj组 件相关知识,我目前也只是实现了一个比较简单的、能用的clipboard data类,并实现了相关逻辑,需要更深入的研究才能搞到图文拷贝黏贴哇。 最后附上一个比较撮的介面和相关interface:
struct Selection
{
Selection():Begin_(-1), End_(-1)
{}
Selection(int nBegin, int nEnd):Begin_(nBegin), End_(nEnd)
{}
int Begin()
{
return Begin_;
}
int End()
{
return End_;
}
int GetSelectionLenght()
{
return (End_ - Begin_ + 1);
}
bool IsSelected() const
{
if ( Begin_ >= 0 && End_ >= 0 && Begin_ <= End_ )
return true;
return false;
}
bool IsBeSelect(int pos) const
{
if (pos <= End_ && pos >= Begin_)
return true;
return false;
}
bool IsBeSelect(int begin, int end) const
{
if (begin <= End_ && end >= Begin_)
return true;
return false;
}
void UpdateSelection(int pos)
{
if (pos <= Begin_)
{
End_ = Begin_;
Begin_ = pos;
}
else
{
End_ = pos;
}
}
void ClearSelection()
{
Begin_ = -1;
End_ = -1;
}
void Log(MY_CHAR* name)
{
ATLTRACE("\n%s: Begin_:%d ; End_:%d;\n"
, name
, Begin_
, End_);
}
int Begin_;
int End_;
};
struct IBaseObj
{
virtual int AddRef() = 0;
virtual int Release() = 0;
};
struct IObj : public IBaseObj
{
virtual const SIZE* GetSize() const = 0;
virtual void OnDraw(HDC dc, RECT& rcObj) = 0;
};
class ContextObj
: public IObj
{
public:
virtual const SIZE* GetSize() const
{
return &sz_;
}
protected:
SIZE sz_;
};
class ImageObj
: public ContextObj
{
public:
ImageObj():obj_(NULL){}
void UpdatePos(int pos, int line)
{
posInLine_ = pos;
ownLine_ = line;
}
protected:
int posInLine_;
int ownLine_;
void* obj_;
};
struct RowObjDetail
{
RowObjDetail()
{
maxHeight_ = 0;
pos_ = -1;
}
int maxHeight_;
int pos_;
};
struct CaretDetail
{
CaretDetail()
{
ZeroMemory(&pt, sizeof(pt));
charPos_ = -1;
line_ = -1;
isLeft = true;
}
CaretDetail& operator = (const CaretDetail& other)
{
charPos_ = other.charPos_;
line_ = other.line_;
pt = other.pt;
isLeft = other.isLeft;
return *this;
}
bool IsEqual(const CaretDetail& other)
{
if (charPos_ == other.charPos_
&& line_ == other.line_
&& isLeft == other.isLeft)
{
return true;
}
return false;
}
void Log(MY_CHAR* name)
{
ATLTRACE("\n%s: charPos_:%d ; line_:%d; pt:[x:%d, y:%d] isLeft:%d \n"
, name
, charPos_
, line_
, pt.x
, pt.y
, isLeft ? 1 : 0);
}
int charPos_;
int line_;
POINT pt;
bool isLeft;
};
struct LinkObj
: public IBaseObj
{
int charPos_;
int nRef_;
MY_CHAR* context_;
LinkObj(int charPos, MY_CHAR* context)
{
int len = lstrlen(context);
context_ = new MY_CHAR[len + 1];
lstrcpy(context_, context);
charPos_ = charPos;
nRef_ = 0;
AddRef();
}
~LinkObj()
{
ClearContext();
}
void ResetContext(MY_CHAR* context)
{
ClearContext();
context_ = context;
}
virtual int AddRef()
{
nRef_++;
return nRef_;
}
virtual int Release()
{
nRef_--;
if (nRef_ == 0)
delete this;
return nRef_;
}
protected:
void ClearContext()
{
if(context_)
delete context_;
}
};
附:优快云的图片上传太挫鸟,上传N次找不到图片到底上传了没有,这鸟设计太垃圾了,郁闷了我半天。 。 。 。
PS:语文水平不行,写得让自己都感觉惭愧呀,欢迎拍砖交流~
源代码下载地址:http://www.oschina.net/code/snippet_564149_10792