一、醉一醉
眨眼功夫,2020年过去一半了。回想最近一段时间的工作和生活,总觉得应该写点儿什么!
于是,最近有空就在想啊想,想想可以写点儿什么有用的东西好呢!刚好之前写过几篇关于高DPI的文章,不知道什么原因,阅读量不是很高,因此打算以高DPI为索引开始引入一系列的控件使用案例,包括Qt自带的控件、简单图表和一些复杂的图表。
- Qt自带的控件就不说了,高DPI框架几乎可以完美适配
- 简单的图表这里主要会引入柱状图、折线图、饼图等
- 复杂图表主要是定制一些股票看盘相关图表,例如分时图、k线图等
对于大众软件来说,友好的支持4k显示器真的很有必要呢。一款好的大众桌面端软件需要适配各种操作系统,从快要被人们遗忘的Xp到现在占有率较高的Win10,如果想拥有一个好的用户体验,高DPI是必须要好好适配滴,这里作者准备了一个系列的高DPI控件适配文章分享给大家,主要整理我工作中遇到的各种控件,适配到已经开发好的高DPI框架中,并做出演示demo,提供给有需要的同学,除过整理已有的控件,更多的是会开发一些更有意义的新控件,比如股票中的分时图、k线等。
目的:
- 推广我自己适配高DPI的方案,供大家讨论,是否有更好的优化空间
- 整理股票相关的控件,适配到我的高DPI框架中,提供给有需要的同学参考
- 阶段性整理我自己的知识库,让零散的知识点汇聚起来
二、效果展示
如下图所示,适配高DPI交互效果。
左右两侧的显示物理尺寸一致,也就是占地面积一样大,不同的是左侧是1080p显示器,右侧是4k显示器
因为是视频录制原因,可能会有视觉误差,实际看的话,左右两个窗体给人的视觉感受大小是一样的。
《来回切换显示器》
《饼图支持操作》
三、高DPI适配
高DPI的适配思路之前已经系统的分析过,详情参看Qt之高DPI显示器(一) - 解决方案整理和Qt之高DPI显示器(二) - 自适配解决方案分析两篇文章。
除过上述两篇文章外,之前计划中还有一篇文章要写,后来实在是太忙了,一直没有写完,第三篇文章主要是想讲下怎么优化现有框架,让DPI适配效率变的更高,后边有机会补上。
本票文章开始算是适配高DPI实践系列文章的开篇之作,饼图控件很早之前就分享过,详情参考Qt之自绘制饼图,怎么绘制饼图这里就不再描述。本篇文章的核心主要是进行了饼图高DPI的显示适配,后边会主要描述下适配的细节,后期还会陆续接入更多更丰富的组件。
下面先带大家回顾下适配高DPI我们都干了哪些事情,最后在看看饼图是怎么适配高DPI的。
1、高DPI框架运作
看过前两篇文章的同学应该知道,我们适配高DPI主要从两个方面进行的,分别是窗体的物理尺寸和字体
物理尺寸
物理尺寸从字面上看就是软件大小,不过我们这里的物理尺寸也包含子窗口的大小,那也就是说子窗口之间的间隙也在物理尺寸这里进行适配。
为了让开发同学无感知的使用,我们新增了同样数量的可能会使用到的界面类,作为我们自己的基础类,并且重写了界面类中跟尺寸相关的函数,让大家使用界面类的时候只换类名称,接口用法还是跟以前一样。
重写了尺寸函数,如果我们也知道当前显示器的DPI,缩放界面是不是就很简单了!当前开发设置的尺寸乘以需要缩放的系数就是最终需要显示的尺寸,框架只需要把最后需要显示的尺寸设置给Qt的接口,也就是当前类的父类接口,这样我们的软件界面就实现了放大。做到这里算不算完呢?仔细想一想,开发同学使用这些接口的时候都是设置了96DPI下的尺寸,如果窗口移动到另一台不同DPI的显示器上,难道我们要把所有set接口重新调用一遍?如果数量少了还行,但一个复杂的软件这样的set接口会多的令你发指,如果你让开发同学都调用一遍,我估计他们会打死你。除过需要调用以外,调用顺序也至关重要,试想这样一种场景,如果需要缩放的窗体很多,你会希望窗体局部突然变大,没有汇率的跳动吗?答案当然是否。
为了让窗体有规律的缩放,那我们就需要控制缩放的顺序了,这里就需要额外的继续重写一些关键方法,之前我们重写类的时候重写了尺寸相关函数,这一次我们还需要把布局相关的函数也进行重写,为的就是在界面布局的时候我们把他们的关系记录下来,后续在出现DPI变化时,我们根据之前维护的布局关系,按层调用每一个需要缩放的界面。
界面布局
这里普及一个知识点,平时我们所看到的软件界面是平面的,是一个二维的概念,但是软件界面在开发的过程中,界面的布局关系其实是一棵树,比如像下图这样的界面,界面A包括了界面B和界面C,而界面B又包括了两个界面D,这样当我们构造两个界面A时,其实所有的界面都被构造了两份,并且界面D被构造了四份。
字体大小
我们适配的高DPI框架,除过自绘文字以外,其他情况是不需要关注字体怎么变动的,这些字体适配都在我们的高DPI适配框架中完成了,这里简单描述下字体适配高DPI的方式。
高DPI框架运行过程中,主动维护了1x、2x和3x下的qss文件,如果检测到程序所需要的Qss文件不在这三个配置中,那么框架会动态的根据一个最接近当前缩放比的qss文件生成一个临时qss文件,比如当前缩放比是1.8x,那么程序将会根据2xqss文件,生成一个适合1.8x缩放比的qss文件,首先就是图片进行压缩显示,字号会乘以1.8然后除以2转换成1.8x缩放比下的字号,可能会有舍入,但是对于大家常用的0.5整数倍缩放比基本都是没有问题的,因为字号一般都是2的整数倍。
2、适配高DPI
高DPI框架设计之初就是想让开发同学尽量少的去操心DPI的事情,但是实际情况是还有少部分情况是需要自己去适配的,比方说自毁界面,这个时候我们就需要自己去获取高DPI相关信息合理绘制
绘制文字
绘制文字时,我们需要获取当前的缩放系数,在合适的实际去缩放绘制相关参数,比方说,当我们绘制12px字体时,如果是在4k显示器下,我们的dpi可能为192,那么缩放系数就是2x,绘制文字时就需要绘制24px字体
缩放系数 = 当前显示器DPI / 96.0
有时候绘制文字时可能会附带限制文字所在区域,96DPI下我们不需要操心区域是否会出现问题,但是如果显示器DPI大于96时,文字绘制的区域我们也就需要相应的适配下,否则可能会出现你不希望的结果。比方说,我们需要在坐标为10,10这个位置,以边框100px正方形框内绘制一段文本,96DPI下可能刚刚好能绘制完这段文字,如果192DPI下,我们把字号放大了一倍,如果绘制区域还是100px的正方形,那么文字很可能连一半都绘制不完。问题出在哪里呢?很显然,字体变大了,我们的绘制区域肯定也需要进行相应的放大,不妨试试200px的正方形是不是可以呢!答案是Yes。
绘制图片
自绘界面时往往少不了绘制图片,下面具体分析下怎么在任意DPI下绘制图片!
首先是获取1x缩放比下的图片路径,然后我们通过一个转换函数转换成我们当前显示器下需要的图片路径,并缩放图片,以达到最好的显示效果。
如下代码所示,是一个封装好的函数,主要完成了根据1x图片路径获取我们将要绘制的图片,并且给我们返回的是内存地址。这里需要额外补充下,Qt中的QPixmap是有做缓存机制的,当我们第二次获取同一张图片时,Qt会直接从内存中获取到上一次图片的内存直接返回给我们,因此这里不需要担心效率问题。
QPixmap TIGERQTCOMM_EXPORT ImagePath::GetStretchPixmap(const std::string & path, float scale)
{
std::string tpath = ImagePath::GetPixmapPath(path, (int)(scale + 0.5001));
float factor = ImagePath::GetStretchFactor(scale);
QPixmap pixmap(tpath.c_str());
if (factor != 1.0)
{
pixmap = pixmap.scaled(pixmap.size() * factor);
}
return pixmap;
}
3、适配饼图
自绘界面时需要我们自己去适配高DPI,主要是绘制所需要的的几何大小需要调整,说的直白一点儿就是看下图,左侧1080P显示器,右侧4K显示器,并且两个显示器尺寸是一样大的。看左右两侧的矩形区域坐标很清楚的展示出来了,右侧看着一样大的举行是左侧举行的两倍大,并且左上角的坐标也是两倍。
左侧矩形几何大小是(83, 104, 168, 211),右侧矩形几何大小是(166, 208, 336, 422) ,按照我们高DPI适配的叫法左侧显示器的缩放比就是1x,右侧是2X
《1080P vs 4K》
前边小节说过了,自绘界面时适配高DPI主要是针对绘制的几何大小,饼图也不例外,这里我贴一个饼图各模块几何大小计算的函数,已经适配过高DPI,方法也很简单
适配过程主要是用宏来完成的,宏定义如下:#define SCALE_NUMBER(n) ((n) * dpi_scale),dpi_scale为每个高DPI框架下类的成员变量,该变量由框架维护,表示当前窗口需要缩放的系数
废话不多说,如下代码是适配过高DPI后的函数,主要是对一些影响几何位置计算的参数进行了缩放。
void CPieChart::ConstructCornerLayout(const QSize & size)
{
int currentR = SCALE_NUMBER(d_ptr->m_MinDiameter);
int diameter;
int horiWidth = size.width();
if (d_ptr->m_bLegendVisible)
{
horiWidth -= SCALE_NUMBER(d_ptr->m_LegendWidth * 2);
}
int PieHeight;
if (d_ptr->m_MutiDay.size() >= 1)
{
PieHeight = size.height() - SCALE_NUMBER(d_ptr->m_BarHeight) * d_ptr->m_MutiDay.size()
- SCALE_NUMBER(d_ptr->m_BottomMargin + d_ptr->m_Space + d_ptr->m_BarSpace * (d_ptr->m_MutiDay.size() - 1))
- SCALE_NUMBER(d_ptr->m_LabelHeight * 2);
}
else
{
PieHeight = size.height();
}
if (horiWidth > PieHeight)
{
diameter = PieHeight;
}
else
{
diameter = horiWidth;
}
int x, y;
int r = diameter - SCALE_NUMBER(d_ptr->m_Minx * 2);
currentR = r > currentR ? r : currentR;
if (d_ptr->m_bLegendVisible)
{
d_ptr->m_Items.resize(4);
x = width() / 2 - currentR / 2;
y = (PieHeight - currentR) / 2;
d_ptr->m_Items[1].m_LegendRect = QRect(SCALE_NUMBER(d_ptr->m_Minx), SCALE_NUMBER(d_ptr->m_Miny)
, SCALE_NUMBER(d_ptr->m_LegendWidth), SCALE_NUMBER(30));
d_ptr->m_Items[0].m_LegendRect = QRect(size.width() - Margin - SCALE_NUMBER(d_ptr->m_LegendWidth)
, SCALE_NUMBER(d_ptr->m_Miny)
, SCALE_NUMBER(d_ptr->m_LegendWidth), SCALE_NUMBER(30));
d_ptr->m_Items[3].m_LegendRect = QRect(size.width() - Margin - SCALE_NUMBER(d_ptr->m_LegendWidth)
, PieHeight - SCALE_NUMBER(d_ptr->m_Miny + 30)
, SCALE_NUMBER(d_ptr->m_LegendWidth), SCALE_NUMBER(30));
d_ptr->m_Items[2].m_LegendRect = QRect(SCALE_NUMBER(d_ptr->m_Minx)
, PieHeight - SCALE_NUMBER(d_ptr->m_Miny + 30)
, SCALE_NUMBER(d_ptr->m_LegendWidth), SCALE_NUMBER(30));
d_ptr->m_Items[0].m_bAlign = false;
d_ptr->m_Items[3].m_bAlign = false;
}
else
{
x = SCALE_NUMBER(d_ptr->m_Minx);
y = SCALE_NUMBER(d_ptr->m_Miny);
}
d_ptr->m_PieRect = QRect(x, y, currentR, currentR);
d_ptr->m_BarsRect = QRect(SCALE_NUMBER(20), 2 * y + currentR + SCALE_NUMBER(d_ptr->m_Space)
, width() - SCALE_NUMBER(50)
, size.height() - PieHeight);
}
到目前为止,本篇文章要分享的内容算基本完成了。饼图控件的其他的代码逻辑,包括绘制逻辑适配高DPI方式都合上述函数类似,大家自行脑补即可。感兴趣的朋友可以到饼图-高DPI下载,CSDN链接中的资源只包含适配过高DPI的饼图绘制代码,仅供大家参考,并不能通过编译。
四、相关文章
值得一看的优秀文章:
很重要--转载声明
本站文章无特别说明,皆为原创,版权所有,转载时请用链接的方式,给出原文出处。同时写上原作者:朝十晚八 or Twowords
如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。