Qt编程Action:Qt的自动反色方案
Qt编程Action:Qt的自动反色方案
笔者最近正在做的事情是制作一款简单的ToDo软件,在编写和设计控件的时候,笔者就注意到一个很经典的问题,我们如何进行科学的反色,从而给编程软件使用者,也就是我们的用户以最好的阅读体验呢?这一篇文章,笔者尝试给出我的一些方案。
我是遇到了什么场景需要思考这个问题?
笔者正在制作Microsoft Edge首页的带有箴言的图片Widget的时候遇到的这个问题。举个例子,笔者打算放置每日自动推送的NASA APOD图像,显示在CCImageWidget上,这个时候,箴言为了方便看清,最好加大图像和文字的颜色对比度。所以摆在我们的面前的依次有如下这些问题:
- 如何计算一个区域下的最好反色色调?
- 上述问题是静态需求,如果我们的
CCImageWidget上出现图片变更,如何进行下一步的计算是高内聚低耦合的?
笔者构思了一下,发现这些都是UI底层问题:
FloatingLabelHelper常是独立顶层窗口,不能直接用parentWidget()->palette()得到真实像素颜色;- 图片可能缩放、裁剪或保持纵横比,需把 widget 坐标映射回 pixmap/image 坐标;
- 图片可能有透明通道(alpha);
- 要在性能与准确度之间权衡。
快速勾勒一下总体方案(流程)
约束好我们的相关接口:首先,背景Widget需要具备裁剪出来自己ViewPort的Pixmap的能力,笔者这里不再赘述如何获取任意QWidget的Pixmap了,假定我们存在一个接口可以获取给定的Widget的Pixmap
QPixmap getWidgetPixmap() const;
和一个设置Pixmap后触发的signal:onPixmapChanged,或者是采用非Qt风格的回调机制,来通知触发自动重新计算发色的办法。这样,我们就可以在图片或浮动位置更新的时候采取以下的行动:
- 计算浮动 Label 的中心(或其他采样点)的全局坐标;
- 把这个点映射到
cover_widget的本地坐标(Qt的mapFromGlobal就是做这个的,将一个全局坐标变化怒到Widget本地坐标) - 把
cover_widget的本地坐标映射为 pixmap/image 的像素坐标(这里我们是考虑缩放/居中/aspect-fit); - 这一步开始我们只需要采样即可,为了迅速,采样我们简单做平均或中位数,得到代表颜色
- 经典的反色方案包含简单亮度法或 WCAG 对比率法,笔者不计划把事情搞得非常的复杂。所以采用简单亮度法
- 将得到的颜色拿来设置Label的字体颜色即可
关键代码
我们直接讨论最核心的关键代码,把思路捋清即可。
把 QLabel 内坐标映射到 pixmap 像素坐标(处理 aspect-fit 居中缩放)
static QPoint widgetPointToPixmapPoint(QLabel* imageLabel, const QPoint& widgetPt) {if (!imageLabel->pixmap() || imageLabel->pixmap()->isNull()) return QPoint(-1, -1);const QPixmap* pm = imageLabel->pixmap();const QSize pixmapSize = pm->size();const QSize widgetSize = imageLabel->size();const double pw = pixmapSize.width();const double ph = pixmapSize.height();const double ww = widgetSize.width();const double wh = widgetSize.height();const double scale = qMin(ww / pw, wh / ph); // Qt代码笔者喜欢入乡随俗,就是用qMin吧!const QSizeF scaledPixmapSize(pw * scale, ph * scale);const double left = (ww - scaledPixmapSize.width()) / 2.0;const double top = (wh - scaledPixmapSize.height()) / 2.0;const double xInPixmap = (widgetPt.x() - left) / scale;const double yInPixmap = (widgetPt.y() - top) / scale;const int ix = qBound(0, int(std::round(xInPixmap)), pixmapSize.width() - 1);const int iy = qBound(0, int(std::round(yInPixmap)), pixmapSize.height() - 1);return QPoint(ix, iy);
}
如果我们的
QLabel::scaledContents为true(像素拉伸),则 scale 计算应改为scaleX = ww/pw,scaleY = wh/ph并分别映射。
在 image 上采样附近平均颜色
static QColor averageColorAround(const QImage& img, const QPoint& center, int radius = 12, int step = 3) {if (center.x() < 0 || center.y() < 0) return QColor();int x0 = qMax(0, center.x() - radius);int x1 = qMin(img.width() - 1, center.x() + radius);int y0 = qMax(0, center.y() - radius);int y1 = qMin(img.height() - 1, center.y() + radius);long long r=0,g=0,b=0,count=0;for (int y=y0; y<=y1; y+=step) {for (int x=x0; x<=x1; x+=step) {QColor c = img.pixelColor(x,y);if (c.alpha() == 0) continue; // 忽略完全透明像素r += c.red(); g += c.green(); b += c.blue();++count;}}if (count == 0) return QColor(); // 全透明或无像素return QColor(int(r/count), int(g/count), int(b/count));
}
radius控制观察范围;step控制采样密度(性能权衡)。
简单亮度法选择黑/白
static inline QColor bestTextColorForBackground(const QColor& bg) {double L = 0.2126 * bg.redF() + 0.7152 * bg.greenF() + 0.0722 * bg.blueF();return (L < 0.5) ? QColor(Qt::white) : QColor(Qt::black);
}
这里我们很容易看到,简单亮度法可以快速的筛选对比色,当然这个就很单调了,我们没办法更好的裁决二维的最佳反色。
性能考虑
我们很容易想到性能问题。这里容易触发性能顾虑的无非这一个点:图像太大了。对于大图像的计算,我们完全可以采用异步的解决方案。
- 使用
QtConcurrent::run()或std::async做averageColorAround的计算;- 计算完后用
QMetaObject::invokeMethod(labelHelper, "setTextColor", Qt::QueuedConnection, Q_ARG(QColor, result))回主线程更新 UI。
或者是自动选择更好的step算法:比如说step=3~5 对通常图片已足够。
当然,还有一种可能我们的确要思考的,就是高频设置图像的场景。对于任何一个只看最终结果的场景,我们完全可以采用延迟分析,比如说,当发现用户正在高频设置图像的时候(Example:大规模触发setPixmap,我们可以手动延迟计算)
Ideas
笔者注:所有的Ideas就是我想到的但是暂时不会做,留给读者朋友思考的:
- 使用中位数而非平均值去抗噪点(
std::nth_element);- 返回三级颜色(白 / 黑 / 柔和灰)以处理中等亮度背景;
- 对结果文字颜色做平滑过渡(
QVariantAnimation)避免闪烁;- 在高分辨率图片上按不同缩放层级自适应
radius;- 若想做更智能的排版,基于局部对比计算是否需要加投影、描边或半透明底栏(轮廓/阴影能显著提升可读性)。
