[Gdiplus]_[中级]_[绘图实现单行文本的多种颜色]

本文介绍了如何使用GDI+在Win32应用中实现单行文本的格式化绘制,特别是支持不同颜色。由于GDI+本身不直接支持富文本,文章提出了一种解决方案,通过截取文本并利用`MeasureCharacterRanges`计算各部分的准确区域,避免多余的空白,实现了单行文本不同部分的样式设置。此外,还提供了相关的代码示例和资源下载链接。
摘要由CSDN通过智能技术生成

场景

  1. 在使用 Gdiplus 绘制文本时, 偶尔会遇到需要支持绘制格式化文本的时候,而 Gdiplus 在这方面并不好,没有官方提供支持的类,也不支持 HTML 标签和 CSS 的样式. 而大部分提供这类富文本的绘制目前我所知道的一种就是通过 Web控件 来处理的. 比如调用 Chromium 的渲染实现.

  2. 另外一种实现就是通过实现自定义的富文本 RichText 控件, QQ 以前就是用的富文本来实现,显示可能是HTML渲染?不过貌似现在它的聊天窗口也不支持一个消息里用两种字体设置不同的文本部分.

说明

  1. Gdiplus 常见的计算文本区域的方法是 graphics.MeasureString. 我们如果要绘制文本局部样式,那么一个方案就是通过把这段文本截取几部分,之后使用 DrawString 方法对不同部分进行设置不同的 FontColor 里进行绘制, 从而不同部分显示不同的样式,问题是我们如何计算不同部分的文本所在的区域 RectF? 使用 MeasureString 分别计算不同的文本部分的 RectF,之后放入一个 vector, 在绘制的时候进行循环绘制。这个方案只能用于单行文本,还有一个问题就是每个部分在绘制时出现了多余的空白区域,导致每个文本部分间隔比较大。

  2. 使用 MeasureString 这个方法会会连文本的周围填充块 pad 都算上. 在Gdiplus里,由于 Graphics 设置了反锯齿之类的效果, 会在绘制时自动拉伸,填充锯齿等行为,导致计算的大小会偏大,并且在使用 DrawString 时如果设置的 RectF 小于它所计算的宽时,会绘制不出来。这个问题目前我还没找到解决办法。比如 MeasureString 得到的 RectF 宽我减少10个像素, 绘制时剩余区域还有,但是文字就是不显示出来, 如图1。

CClientDC dc(m_hWnd);
Gdiplus::Graphics graphics(dc);
graphics.SetPixelOffsetMode(Gdiplus::PixelOffsetModeHighQuality);
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintSystemDefault);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);

图1:
在这里插入图片描述

  1. Gdiplus 提供了 graphics.MeasureCharacterRanges 来获取一段范围文字的区域 Gdiplus::Region. 通过区域的 Region->GetBounds()来获取不包括外边填充 pad 部分的文本大小。我们借此来获取文本的最小宽度. 之后再通过把下一段文本的X坐标向左偏移的方式覆盖前面文本的 pad 部分来达到无多余空白区域的效果。
    如图2:

图2:
在这里插入图片描述

  1. 我封装了一个类,便于实现绘制单行的文本样式。至于换行的实现,暂时还没有好的方案.

代码

string_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*> GDIStringsFormat;

class StringDrawObject
{
public:
	StringDrawObject();

	inline StringDrawObject& SetFont(Gdiplus::Font* font){
		font_ = font;
		return *this;
	}

	inline StringDrawObject& SetColor(Gdiplus::Color* color){
		color_ = color;
		return *this;
	}

	inline StringDrawObject& SetString(const wchar_t* str){
		str_ = str;
		return *this;
	}

	inline StringDrawObject& 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;
	}

	inline void ClearGDIStringsFormat(){
		sf_.clear();
	}

	inline void ClearSfValueMap(){
		sf_value_map_.clear();
	}

	inline StringDrawObject& SetOffsetXY(int offsetx,int offsety){
		offsetx_ = offsetx;
		offsety_ = offsety;
		return *this;
	}

	void CalcGDIStringsFormat(Gdiplus::Graphics* graphics);
	void DrawGDIStringsFormat(Gdiplus::Graphics* graphics);

private:
	Gdiplus::Font* font_;
	wstring str_;
	Gdiplus::Color* color_;
	map<wstring,tuple<wstring,Gdiplus::Color*,Gdiplus::Font*>> sf_value_map_;
	int offsetx_;
	int offsety_;
	vector<GDIStringsFormat> sf_;
};

#endif

string_draw_object.cpp

#include "stdafx.h"
#include "string_draw_object.h"
#include <regex>
#include <assert.h>

using namespace Gdiplus;

StringDrawObject::StringDrawObject()
{
	font_ = NULL;
	color_ = NULL;
	offsetx_ = 0;
	offsety_ = 0;
}

void StringDrawObject::DrawGDIStringsFormat(Gdiplus::Graphics* graphics)
{
	StringFormat strFormat;

	// 测试
	//Color color_green(Color::Green);
	//SolidBrush brush1(color_green);
	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_);

		//if(i%2 == 0)
		//	graphics->FillRectangle(&brush1,rect_one);

		SolidBrush brush(*color);
		graphics->DrawString(str_one.c_str(),str_one.size(),font,rect_one,&strFormat,&brush);
	}
}

// https://stackoverflow.com/questions/11708621/how-to-measure-width-of-a-string-precisely
// https://bbs.csdn.net/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 StringDrawObject::CalcGDIStringsFormat(Gdiplus::Graphics* graphics)
{
	assert(str_.size() && font_ && color_ && 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 index = 0;
	StringFormat strFormat;
	strFormat.SetFormatFlags(StringFormatFlagsMeasureTrailingSpaces);
	StringFormat strFormatNoSpace;
	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);

		
		Region* r1 = NULL;
		RectF rect_temp;
		Gdiplus::RectF rect_default;
		if(str1.size()){
			strFormat.SetFormatFlags(StringFormatFlagsMeasureTrailingSpaces);
			r1 = new Region[1];
			rect_default.X = posx;
			graphics->MeasureString(str1.c_str(),str1.size(),font_,rect_default,&strFormat,&rect_default);

			CharacterRange charRanges[1] = { CharacterRange(0,str1.size())};
			strFormat.SetMeasurableCharacterRanges (1,charRanges);
			graphics->MeasureCharacterRanges(str1.c_str(), -1,font_, rect_default, &strFormat, 1, r1);
			r1->GetBounds(&rect_temp,graphics);
			rect_temp.X = rect_default.X;
			sf_.push_back(GDIStringsFormat(str1,rect_default,color_,font_));
			delete []r1;
		}
		

		///
		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);
		}

		strFormatNoSpace.SetFormatFlags(StringFormatFlagsMeasureTrailingSpaces);
		r1 = new Region[1];
		Gdiplus::RectF rect_attrib;
		rect_attrib.X = rect_temp.GetRight();
		graphics->MeasureString(str2.c_str(),str2.size(),font_value,rect_attrib,&strFormatNoSpace,&rect_attrib);
		
		CharacterRange charRanges[1] = {CharacterRange(0,str2.size())};
		strFormatNoSpace.SetMeasurableCharacterRanges (1,charRanges);
		graphics->MeasureCharacterRanges(str2.c_str(), -1,font_value, rect_attrib, &strFormatNoSpace, 1, r1);
		r1->GetBounds(&rect_temp,graphics);
		rect_temp.X = rect_attrib.X;
		posx = rect_temp.GetRight();

		// 可能前面没字符串
		if(rect_default.Height)
			rect_attrib.Y = rect_default.Y+(rect_default.Height-rect_attrib.Height)/2;

		delete []r1;
		sf_.push_back(GDIStringsFormat(str2,rect_attrib,color_value,font_value));

		index = pos+length;
	}

	if(index+1 <= str_.size()){
		auto xstr = str_.substr(index);
		Gdiplus::RectF rect_default;
		rect_default.X = posx;
		graphics->MeasureString(xstr.c_str(),xstr.size(),font_,rect_default,&strFormat,&rect_default);
		sf_.push_back(GDIStringsFormat(xstr,rect_default,color_,font_));
	}
}

调用 View.cpp

// View.cpp : implementation of the CView class
//
/

#include "stdafx.h"
#include "resource.h"
#include "atlmisc.h"
#include "View.h"

using namespace Gdiplus;

BOOL CView::PreTranslateMessage(MSG* pMsg)
{
	pMsg;
	return FALSE;
}

int CView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	myview_.Create(m_hWnd,CRect(0,0,100,100));
	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);

	/// 应用字符串格式化 //
	CClientDC dc(m_hWnd);
	Gdiplus::Graphics graphics(dc);
	graphics.SetPixelOffsetMode(Gdiplus::PixelOffsetModeHighQuality);
	graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintSystemDefault);
	graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);

	sdo_.SetString(L"今天是考试得了 %number% 分, 因为我从 %url% 那里获得了指导.");
	sdo_.SetFont(font_);
	sdo_.SetColor(&color_black_);
	sdo_.SetOffsetXY(10,10);
	sdo_.SetKeyValue(L"%number%",L"100",&color_red_,font_);
	sdo_.SetKeyValue(L"%url%",L"https://infoworld.blog.csdn.net",&color_blue_,font_bold_);
	sdo_.CalcGDIStringsFormat(&graphics);

	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);

	//背景图
	Gdiplus::Graphics graphics(mdc);
	graphics.SetPixelOffsetMode(Gdiplus::PixelOffsetModeHighQuality);
	graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintSystemDefault);
	graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);

	sdo_.DrawGDIStringsFormat(&graphics);

	return 0;
}

项目下载

https://download.csdn.net/download/infoworld/12261663

参考

how-to-measure-width-of-a-string-precisely

c# winfrom GDI+ DrawString怎么画不同颜色的字

measurestring-pads-the-text-on-the-left-and-the-right

measurecharacterranges

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Peter(阿斯拉达)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值