场景
- 在开发
WTL/WIN32
界面程序时,有时候需要绘制多行的段落文本,但是文本里的数值需要设置红色以便能让人第一时间关注到它。这种文本可以称之为富文本。GDI
的DrawText
和GDIPlus
的DrawString
方法都只能连续绘制某个颜色的文本。怎么实现?
说明
-
在《绘图实现单行文本的多种颜色》[1]里介绍了绘制单行多种颜色文本的实现,但是并不支持换行,换行需要知道上一行绘制的文本长度和换行后的文本起始位置,实现起来并不容易。
-
要实现多行富文本,那么关键的方法是计算一行字符串在给定以下的
3
个重要的参数计算当前行需要绘制的字符个数,之后把剩余的字符串交给下一行绘制。方法calcOneLineFitStringLength
就是只存储给定行剩余宽度绘制适合个数的字符串,把剩余的字符递归传递给calcOneLineFitStringLength
本身计算并存储。-
起始横坐标
x
-
纵坐标
y
-
最大行宽度
maxWidth
std::pair<float,float> StringMultiLineDrawObject::calcOneLineFitStringLength(Gdiplus::Graphics* graphics, const wchar_t* str, int startX, int startY, int maxWidth,int lineSpace,Gdiplus::Font* font,Gdiplus::Color* color)
-
-
在计算字符串在哪个长度时换行,可以通过行的像素长度,字符串总个数,绘制字符串需要的像素长度来按比例计算最接近的可以在当前行绘制完的字符串个数。
auto startWidth = maxWidth - startX; // 行的像素长度
int mLen = startWidth * len / (int)rect_default.Width; // len:字符串总个数 rect_default.Width: 绘制字符串需要的像素长度
-
Gdi
和Gdiplus
实现都是类似的,以下例子给了两种库的实现。类StringMultiLineDrawObject
封装了设置替换字符串和计算存储字符串绘制的数组对象vector<GDIPlusStringsFormat>
。这个对象在绘制的时候调用drawGDIPlusStringsFormat
方法时会枚举对象并绘制。当然计算的时候可以默认x
,y
坐标为0
,通过setOffsetXY
来设置实际的界面x
,y
坐标。 -
注意,这里查找各部分独立绘制的字符串是通过关键字如
%who%
来替换为真实字符串来设置的,比较省事。大家可以实现类似[1]实际字符串[2]
这种方式的字符串提取。
auto str = L"%who1%今天是考试得了%number%分, 因为我从一个很好的技术作者网站%url%那里获得了指导.谢谢%who2%.";
...
sdoGdiplus_.setKeyValue(L"%who1%",L"GDIPLUS",&color_blue_,font_);
sdoGdiplus_.setKeyValue(L"%who2%",L"Tobey",&color_blue_,font_);
sdoGdiplus_.setKeyValue(L"%number%",L"100",&color_red_,font_);
sdoGdiplus_.setKeyValue(L"%url%",L"https://blog.youkuaiyun.com/infoworld",&color_blue_,font_);
例子
- 以下项目例子实现了
GDI
和GDIPlus
绘制多行多种颜色的文本(富文本)。CView
类调用富文本实现StringMultiLineDrawObject
。
string_multi_line_draw_object.h
#ifndef STRING_DRAW_UTIL_H
#define STRING_DRAW_UTIL_H
#include <Windows.h>
#include <vector>
#include <map>
#include <string>
#include <GdiPlus.h>
#include <tuple>
using namespace std;
typedef tuple<wstring,Gdiplus::RectF,Gdiplus::Color*,Gdiplus::Font*> GDIPlusStringsFormat;
typedef tuple<wstring,CRect,COLORREF,HFONT> GDIStringsFormat;
class StringMultiLineDrawObject
{
public:
StringMultiLineDrawObject();
inline StringMultiLineDrawObject& setOffsetXY(int offsetx,int offsety){
offsetx_ = offsetx;
offsety_ = offsety;
return *this;
}
inline StringMultiLineDrawObject& setString(const wchar_t* str){
str_ = str;
return *this;
}
std::wstring getSimpleString();
public:
inline StringMultiLineDrawObject& setFont(Gdiplus::Font* font){
font_ = font;
return *this;
}
inline StringMultiLineDrawObject& setColor(Gdiplus::Color* color){
color_ = color;
return *this;
}
inline StringMultiLineDrawObject& setKeyValue(const wchar_t* key,
const wchar_t* value,Gdiplus::Color* color,Gdiplus::Font* font){
sf_value_map_[key] = std::tuple<wstring,Gdiplus::Color*,Gdiplus::Font*>(value,color,font);
return *this;
}
void calcGDIPlusStringsFormat(Gdiplus::Graphics* graphics,int maxWidth,int lineSpace);
void drawGDIPlusStringsFormat(Gdiplus::Graphics* graphics);
protected:
void measureString(Gdiplus::Graphics* graphics, const wchar_t* str, int strLen,
Gdiplus::Font* font,Gdiplus::StringFormat* strFormat, Gdiplus::RectF& rect_default);
std::pair<float,float> calcOneLineFitStringLength(Gdiplus::Graphics* graphics, const wchar_t* str,
int startX,int startY, int maxWidth,int lineSpace,Gdiplus::Font* font,Gdiplus::Color* color);
public:
inline StringMultiLineDrawObject& setFont(HFONT font){
hfont_ = font;
return *this;
}
inline StringMultiLineDrawObject& setColor(COLORREF color){
hcolor_ = color;
return *this;
}
inline StringMultiLineDrawObject& setKeyValue(const wchar_t* key,
const wchar_t* value,COLORREF color,HFONT font){
hsf_value_map_[key] = std::tuple<wstring,COLORREF,HFONT>(value,color,font);
return *this;
}
void calcGDIStringsFormat(CDC& cdc,int maxWidth,int lineSpace);
void drawGDIStringsFormat(CDC& cdc);
protected:
void measureString(CDC& cdc, const wchar_t* str, int strLen, HFONT font, CRect& rect_default);
std::pair<float,float> calcOneLineFitStringLength(CDC& cdc, const wchar_t* str,
int startX,int startY, int maxWidth,int lineSpace,HFONT font,COLORREF color);
private:
Gdiplus::Font* font_ = nullptr;
Gdiplus::Color* color_= nullptr;
map<wstring,tuple<wstring,Gdiplus::Color*,Gdiplus::Font*>> sf_value_map_;
vector<GDIPlusStringsFormat> sf_;
private:
HFONT hfont_ = NULL;
COLORREF hcolor_ = 0;
map<wstring,tuple<wstring,COLORREF,HFONT>> hsf_value_map_;
vector<GDIStringsFormat> hsf_;
private:
int offsetx_;
int offsety_;
wstring str_;
wstring allString_;
float defaultStringHeight_ = 0;
};
#endif
string_multi_line_draw_object.cpp
#include "stdafx.h"
#include "string_multi_line_draw_object.h"
#include <regex>
#include <assert.h>
using namespace Gdiplus;
StringMultiLineDrawObject::StringMultiLineDrawObject()
{
offsetx_ = 0;
offsety_ = 0;
}
void StringMultiLineDrawObject::drawGDIPlusStringsFormat(Gdiplus::Graphics* graphics)
{
StringFormat strFormat;
Gdiplus::Pen pen(*color_);
for(int i =0;i< sf_.size();++i){
auto& one = sf_[i];
auto& str_one = std::get<0>(one);
auto rect_one = std::get<1>(one);
auto color = std::get<2>(one);
auto font = std::get<3>(one);
rect_one.Offset(offsetx_,offsety_);
// -- 绘制计量矩形框调试
// graphics->DrawRectangle(&pen, rect_one);
SolidBrush brush(*color);
if (font != font_)
rect_one.Y = rect_one.Y - (rect_one.Height - defaultStringHeight_) / 2;
graphics->DrawString(str_one.c_str(),str_one.size(),font,rect_one,&strFormat,&brush);
}
}
void StringMultiLineDrawObject::measureString(Gdiplus::Graphics* graphics, const wchar_t* str,int strLen,
Gdiplus::Font* font,Gdiplus::StringFormat* strFormat, Gdiplus::RectF& rect_default)
{
graphics->MeasureString(str, strLen,font,Gdiplus::PointF(0,0),strFormat,&rect_default);
}
std::pair<float,float> StringMultiLineDrawObject::calcOneLineFitStringLength(Gdiplus::Graphics* graphics, const wchar_t* str,
int startX, int startY, int maxWidth,int lineSpace,Gdiplus::Font* font,Gdiplus::Color* color)
{
if (!str)
return std::make_pair(startX,startY);
StringFormat strFormatNoSpace;
Gdiplus::RectF rect_default;
auto len = wcslen(str);
measureString(graphics, str, len, font, &strFormatNoSpace, rect_default);
auto startWidth = maxWidth - startX;
if (rect_default.Width > startWidth) {
int strHeight = rect_default.Height;
int mLen = startWidth * len / (int)rect_default.Width;
measureString(graphics, str, mLen, font, &strFormatNoSpace, rect_default);
if (rect_default.Width > startWidth) {
while (mLen--) {
// minus one character
measureString(graphics, str, mLen, font, &strFormatNoSpace, rect_default);
if (rect_default.Width > startWidth){
continue;
}else {
std::wstring sub(str, mLen);
rect_default.X = startX;
rect_default.Y = startY;
sf_.push_back(GDIPlusStringsFormat(sub,rect_default,color,font));
return calcOneLineFitStringLength(graphics, str + mLen, 0,
startY + strHeight + lineSpace, maxWidth,lineSpace, font,color);
}
}
}else {
// new line
if (mLen) {
std::wstring sub(str, mLen);
rect_default.X = startX;
rect_default.Y = startY;
sf_.push_back(GDIPlusStringsFormat(sub,rect_default,color,font));
return calcOneLineFitStringLength(graphics, str + mLen, 0,
startY + strHeight + lineSpace, maxWidth, lineSpace,font,color);
}else {
return calcOneLineFitStringLength(graphics, str, 0,
startY + strHeight + lineSpace, maxWidth, lineSpace,font,color);
}
}
}else {
rect_default.X = startX;
rect_default.Y = startY;
sf_.push_back(GDIPlusStringsFormat(str,rect_default,color,font));
return std::make_pair(rect_default.GetRight(), startY);
}
return std::make_pair(startX,startY);
}
// https://stackoverflow.com/questions/11708621/how-to-measure-width-of-a-string-precisely
// https://bbs.youkuaiyun.com/topics/391929399?page=1
// https://stackoverflow.com/questions/118686/measurestring-pads-the-text-on-the-left-and-the-right
// https://docs.microsoft.com/en-us/windows/win32/api/gdiplusgraphics/nf-gdiplusgraphics-graphics-measurecharacterranges
void StringMultiLineDrawObject::calcGDIPlusStringsFormat(Gdiplus::Graphics* graphics,int maxWidth,int lineSpace)
{
defaultStringHeight_ = font_->GetHeight(graphics);
wstring ptext;
for(auto ite = sf_value_map_.begin();ite != sf_value_map_.end();++ite){
auto& key = ite->first;
ptext.append(key).append(L"|");
}
ptext.erase(ptext.end()-1);
wregex pattern1(ptext);
auto words_begin =
wsregex_iterator(str_.begin(),str_.end(), pattern1);
auto words_end = std::wsregex_iterator();
wsmatch match;
int posx = 0;
int posy = 0;
int index = 0;
allString_.clear();
for (std::wsregex_iterator i = words_begin; i != words_end; ++i){
std::wsmatch match = *i;
size_t pos = match.position();
size_t length = match.length();
wstring str1 = str_.substr(index,pos-index);
wstring str2 = str_.substr(pos,length);
////////////////////////////
if(str1.size()){
auto pp = calcOneLineFitStringLength(graphics, str1.c_str(), posx, posy, maxWidth, lineSpace, font_, color_);
posx = pp.first;
posy = pp.second;
}
///////////////////////////
auto ite = sf_value_map_.find(str2);
auto font_value = font_;
Color* color_value = color_;
if(ite != sf_value_map_.end()){
auto& tt = ite->second;
str2 = get<0>(tt);
color_value = get<1>(tt);
font_value = get<2>(tt);
}
auto pp = calcOneLineFitStringLength(graphics, str2.c_str(), posx, posy, maxWidth, lineSpace, font_value, color_value);
posx = pp.first;
posy = pp.second;
allString_.append(str2);
index = pos+length;
}
if(index+1 <= str_.size()){
auto xstr = str_.substr(index);
auto pp = calcOneLineFitStringLength(graphics, xstr.c_str(), posx, posy, maxWidth, lineSpace, font_, color_);
posx = pp.first;
posy = pp.second;
allString_.append(xstr);
}
}
std::wstring StringMultiLineDrawObject::getSimpleString()
{
return allString_;
}
void StringMultiLineDrawObject::calcGDIStringsFormat(CDC& cdc, int maxWidth, int lineSpace)
{
CFontHandle font(hfont_);
LOGFONT lfont;
font.GetLogFont(lfont);
defaultStringHeight_ = lfont.lfHeight;
wstring ptext;
for(auto ite = hsf_value_map_.begin();ite != hsf_value_map_.end();++ite){
auto& key = ite->first;
ptext.append(key).append(L"|");
}
ptext.erase(ptext.end()-1);
wregex pattern1(ptext);
auto words_begin =
wsregex_iterator(str_.begin(),str_.end(), pattern1);
auto words_end = std::wsregex_iterator();
wsmatch match;
int posx = 0;
int posy = 0;
int index = 0;
allString_.clear();
for (std::wsregex_iterator i = words_begin; i != words_end; ++i){
std::wsmatch match = *i;
size_t pos = match.position();
size_t length = match.length();
wstring str1 = str_.substr(index,pos-index);
wstring str2 = str_.substr(pos,length);
////////////////////////////
if(str1.size()){
auto pp = calcOneLineFitStringLength(cdc, str1.c_str(), posx, posy, maxWidth, lineSpace, hfont_, hcolor_);
posx = pp.first;
posy = pp.second;
}
///////////////////////////
auto ite = hsf_value_map_.find(str2);
auto font_value = hfont_;
auto color_value = hcolor_;
if(ite != hsf_value_map_.end()){
auto& tt = ite->second;
str2 = get<0>(tt);
color_value = get<1>(tt);
font_value = get<2>(tt);
}
auto pp = calcOneLineFitStringLength(cdc, str2.c_str(), posx, posy, maxWidth, lineSpace, font_value, color_value);
posx = pp.first;
posy = pp.second;
allString_.append(str2);
index = pos+length;
}
if(index+1 <= str_.size()){
auto xstr = str_.substr(index);
auto pp = calcOneLineFitStringLength(cdc, xstr.c_str(), posx, posy, maxWidth, lineSpace, hfont_, hcolor_);
posx = pp.first;
posy = pp.second;
allString_.append(xstr);
}
}
void StringMultiLineDrawObject::drawGDIStringsFormat(CDC& cdc)
{
for(int i =0;i< hsf_.size();++i){
auto& one = hsf_[i];
auto& str_one = std::get<0>(one);
auto rect_one = std::get<1>(one);
auto color = std::get<2>(one);
auto font = std::get<3>(one);
rect_one.OffsetRect(CPoint(offsetx_, offsety_));
if (font != hfont_)
rect_one.top = rect_one.top - (rect_one.Height() - defaultStringHeight_) / 2;
auto oldFont = cdc.SelectFont(font);
cdc.SetTextColor(color);
cdc.DrawText(str_one.c_str(), str_one.size(), rect_one, DT_LEFT);
cdc.SelectFont(oldFont);
}
}
void StringMultiLineDrawObject::measureString(CDC& cdc, const wchar_t* str, int strLen,
HFONT font, CRect& rect_default)
{
auto oldFont = cdc.SelectFont(font);
CSize size;
cdc.GetTextExtent(str, strLen, &size);
rect_default.right = rect_default.left + size.cx;
rect_default.bottom = rect_default.top + size.cy;
cdc.SelectFont(oldFont);
}
std::pair<float, float> StringMultiLineDrawObject::calcOneLineFitStringLength(CDC& cdc, const wchar_t* str,
int startX, int startY, int maxWidth, int lineSpace, HFONT font, COLORREF color)
{
if (!str)
return std::make_pair(startX,startY);
CRect rect_default;
auto len = wcslen(str);
measureString(cdc, str, len, font, rect_default);
auto startWidth = maxWidth - startX;
if (rect_default.Width() > startWidth) {
int strHeight = rect_default.Height();
int mLen = startWidth * len / (int)rect_default.Width();
measureString(cdc, str, mLen, font, rect_default);
if (rect_default.Width() > startWidth) {
while (mLen--) {
// minus one character
measureString(cdc, str, mLen, font, rect_default);
if (rect_default.Width() > startWidth){
continue;
}else {
std::wstring sub(str, mLen);
rect_default.MoveToXY(startX, startY);
hsf_.push_back(GDIStringsFormat(sub,rect_default,color,font));
return calcOneLineFitStringLength(cdc, str + mLen, 0,
startY + strHeight + lineSpace, maxWidth,lineSpace, font,color);
}
}
}else {
// new line
if (mLen) {
std::wstring sub(str, mLen);
rect_default.MoveToXY(startX, startY);
hsf_.push_back(GDIStringsFormat(sub,rect_default,color,font));
return calcOneLineFitStringLength(cdc, str + mLen, 0,
startY + strHeight + lineSpace, maxWidth, lineSpace,font,color);
}else {
return calcOneLineFitStringLength(cdc, str, 0,
startY + strHeight + lineSpace, maxWidth, lineSpace,font,color);
}
}
}else {
rect_default.MoveToXY(startX, startY);
hsf_.push_back(GDIStringsFormat(str,rect_default,color,font));
return std::make_pair(rect_default.right, startY);
}
return std::make_pair(startX,startY);
}
View.h
// View.h : interface of the CView class
//
/////////////////////////////////////////////////////////////////////////////
#pragma once
#include "MyView.h"
#include "test-string-draw-object.h"
#include "string_multi_line_draw_object.h"
class CView : public CWindowImpl<CView>
{
public:
DECLARE_WND_CLASS(NULL)
BOOL PreTranslateMessage(MSG* pMsg);
BEGIN_MSG_MAP_EX(CView)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
MSG_WM_CREATE(OnCreate)
END_MSG_MAP()
// Handler prototypes (uncomment arguments if needed):
// LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
// LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
// LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)
int OnCreate(LPCREATESTRUCT lpCreateStruct);
LRESULT OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
void ShowMyView();
protected:
StringMultiLineDrawObject sdoGdiplus_;
Gdiplus::Color color_black_;
Gdiplus::Color color_red_;
Gdiplus::Color color_blue_;
Gdiplus::Font* font_ = nullptr;
Gdiplus::Font* font_bold_ = nullptr;
MyView myview_;
StringMultiLineDrawObject sdoGdi_;
COLORREF hcolor_black_;
COLORREF hcolor_red_;
COLORREF hcolor_blue_;
HFONT hfont_ = NULL;
HFONT hfont_bold_ = NULL;
};
View.cpp
// View.cpp : implementation of the CView class
//
/////////////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "resource.h"
#include "atlmisc.h"
#include "View.h"
using namespace Gdiplus;
static HFONT GetHFONT(int em,int charset,
bool bold,const wchar_t* fontName)
{
LOGFONT lf;
memset(&lf, 0, sizeof(LOGFONT)); // zero out structure
lf.lfHeight = em; // request a 8-pixel-height font
lf.lfCharSet = charset;
lstrcpy(lf.lfFaceName,fontName); // request a face name "Arial"
if(bold)
lf.lfWeight = FW_BOLD;
else
lf.lfWeight = FW_NORMAL;
HFONT font = ::CreateFontIndirect(&lf);
return font;
}
BOOL CView::PreTranslateMessage(MSG* pMsg)
{
pMsg;
return FALSE;
}
int CView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
myview_.Create(m_hWnd,CRect(0,0,100,100));
auto str = L"%who1%今天是考试得了%number%分, 因为我从一个很好的技术作者网站%url%那里获得了指导.谢谢%who2%.";
/////////////// 应用字符串格式化 //////////////////
hfont_ = GetHFONT(16, DEFAULT_CHARSET, false, L"Arial");
hfont_bold_ = GetHFONT(24, DEFAULT_CHARSET, true, L"Arial");
hcolor_black_ = RGB(0, 0, 0);
hcolor_blue_ = RGB(0, 0, 255);
hcolor_red_ = RGB(255, 0, 0);
CClientDC dc(m_hWnd);
sdoGdi_.setString(str);
sdoGdi_.setFont(hfont_);
sdoGdi_.setColor(hcolor_black_);
sdoGdi_.setOffsetXY(10,10);
sdoGdi_.setKeyValue(L"%who1%",L"GDI",hcolor_blue_,hfont_);
sdoGdi_.setKeyValue(L"%who2%",L"Tobey",hcolor_blue_,hfont_);
sdoGdi_.setKeyValue(L"%number%",L"100",hcolor_red_,hfont_);
sdoGdi_.setKeyValue(L"%url%",L"https://blog.youkuaiyun.com/infoworld",hcolor_blue_,hfont_bold_);
sdoGdi_.calcGDIStringsFormat(dc,500,10);
/////////////////// Gdiplus //////////////////////
Gdiplus::Graphics graphics(m_hWnd);
graphics.SetPixelOffsetMode(Gdiplus::PixelOffsetModeHighQuality);
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintSystemDefault);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
Gdiplus::FontFamily fontFamily(L"Arial");
font_ = new Gdiplus::Font(&fontFamily,16,
Gdiplus::FontStyleRegular,Gdiplus::UnitPixel);
font_bold_ = new Gdiplus::Font(&fontFamily,24,
Gdiplus::FontStyleBold,Gdiplus::UnitPixel);
color_black_.SetValue(Gdiplus::Color::Black);
color_red_.SetValue(Gdiplus::Color::Red);
color_blue_.SetValue(Gdiplus::Color::Blue);
sdoGdiplus_.setString(str);
sdoGdiplus_.setFont(font_);
sdoGdiplus_.setColor(&color_black_);
sdoGdiplus_.setOffsetXY(10,200);
sdoGdiplus_.setKeyValue(L"%who1%",L"GDIPLUS",&color_blue_,font_);
sdoGdiplus_.setKeyValue(L"%who2%",L"Tobey",&color_blue_,font_);
sdoGdiplus_.setKeyValue(L"%number%",L"100",&color_red_,font_);
sdoGdiplus_.setKeyValue(L"%url%",L"https://blog.youkuaiyun.com/infoworld",&color_blue_,font_);
sdoGdiplus_.calcGDIPlusStringsFormat(&graphics,500,10);
return 0;
}
void CView::ShowMyView()
{
myview_.ShowWindow(SW_SHOW);
}
LRESULT CView::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
CPaintDC hdc(m_hWnd);
CRect client_rect;
GetClientRect(&client_rect);
CMemoryDC mdc(hdc,client_rect);
mdc.FillSolidRect(client_rect,RGB(255,255,255));
mdc.SetBkMode(TRANSPARENT);
// GDI
sdoGdi_.drawGDIStringsFormat(mdc);
//背景图
Gdiplus::Graphics graphics(mdc);
graphics.SetPixelOffsetMode(Gdiplus::PixelOffsetModeHighQuality);
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintSystemDefault);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
// GDIPlus
sdoGdiplus_.drawGDIPlusStringsFormat(&graphics);
return 0;
}
项目
图1:
下载地址: