我们在缩放图片时,都期望能保持图片的原长宽比,因为这样可以防止图片变形,但往往事与愿违,有些时候我们没办法保持原图的长宽比不变,比如需要在保持图片高度不变的情况下,仅横向拉伸图片,此时就会导致图片变形。

为了解决这种问题,我们可以考虑使用九宫格模式进行图片的缩放。

九宫格模式就是将图片切分为九块(不强制切分的每一块必须等分),如下图所示,在图片缩放时,我们通常保持1、3、7、9四个顶点位置的图片不变,对2、4、5、6、8五个区域进行缩放。

1. 使用QSS实现

在Qt中实现图片按九宫格缩放最简单的方法是使用QSS的border-image属性来实现,通过上下左右四个边框的宽度或高度来设置1、3、7、9四个顶点的大小。

1
2
3
4
5
border-image: url(:/Sample/image/NinePatchTest.png);
border-left: 210px;
border-top: 180px;
border-right: 300px;
border-bottom: 227px;

为了便于测试,我制作了一个九宫格测试图片(整个图片的宽高为1280*720):

通过这种方法实现的九宫格缩放有一个弊端:

该方案会始终保持1、3、7、9四个区域图片的宽高不变,如1号区域是210*180,当我们对图片进行放大时,这个行为是没有问题的,但当我们缩小图片时,缩小后的图片宽高不足以容纳4个顶角图片时,缩小操作就会出现非预期的行为,可能如下图所示:

这种弊端还会体现在支持高DPI缩放的Qt程序中,例如在一个支持DPI缩放的程序中,为宽高为640*360的QWidget设置背景图片,我们知道不同的 devicePixelRatio 会导致实际渲染的图片宽高不同,如devicePixelRatio为1时,图片渲染区域为640*360, 而devicePixelRatio为2时,实际渲染区域为1280*720,为了保证程序在不同DPI的环境下都有比较好的界面呈现,我们通常简单得指定一个倍图(如2倍图)让程序自动进行图片缩放(当然也可以使用SVG或针对不同DPI使用不同的图片)。

而使用 border-left 等属性指定的宽度是固定的,顶点图片无法参与到自动缩放中来,从而会导致这一 DPI 适配机制失效。下一节介绍的方法可以解决这一弊端。

2. 使用代码实现九宫格缩放

本节介绍如何使用代码来实现九宫格模式缩放图片,实现原理大致如下:

提取九个区域的图片,保持1、3、7、9四个图片不变,对2、4、5、6、8五个区域图片进行缩放,分别得到新的图片,最后再将九个区域的图片合并成一张图。

为了解决上述QSS方案的弊端,我们可以采取先将图片在保持长宽比的情况缩放到相应尺寸:

1
QPixmap keepRatioScaledPix = src.scaled(destSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);

由于keepRatioScaledPix实际尺寸肯定与我们想要得到的destSize不匹配,所以我们再把keepRatioScaledPix图片按九宫格模式缩放。

具体实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
QPixmap ScaleByNinePatch(const QPixmap& src, // 原图
QSize destSize, // 目标尺寸
bool keepVertexImageSize, // 是否保持1、3、7、9顶点图片大小不变,设置为true则与QSS方式效果类似(仅类似,因为该函数是直接返回原图)
int pos1Width,
int pos1Height,
int pos3Width,
int pos7Height,
Qt::TransformationMode mode = Qt::SmoothTransformation) {
// 参数校准
pos1Width = qMax(pos1Width, 0);
pos1Height = qMax(pos1Height, 0);
pos3Width = qMax(pos3Width, 0);
pos7Height = qMax(pos7Height, 0);

int handlePixW = 0, handlePixH = 0;
const QPixmap* handlePix = nullptr;
QPixmap keepRatioScaledPix;

if (keepVertexImageSize) {
// 保持顶点图片大小不变的情况,如果顶点图片宽或高已经大于了目标宽高,则直接返回原图
if (pos1Width + pos3Width > destSize.width() || pos1Height + pos7Height > destSize.height())
return src;

handlePixW = src.width();
handlePixH = src.height();
handlePix = &src;
}
else {
const qreal srcW = src.width();
const qreal srcH = src.height();

// 先等比缩放到适合的尺寸
keepRatioScaledPix = src.scaled(destSize, Qt::KeepAspectRatio, mode);
handlePixW = keepRatioScaledPix.width();
handlePixH = keepRatioScaledPix.height();
qreal krsPixWidth = handlePixW;
qreal krsPixHeight = handlePixH;

// 按照之前的比例获取新顶点图片的大小
pos1Width = krsPixWidth * (qreal)pos1Width / srcW;
pos1Height = krsPixHeight * (qreal)pos1Height / srcH;
pos3Width = krsPixWidth * (qreal)pos3Width / srcW;
pos7Height = krsPixHeight * (qreal)pos7Height / srcH;

handlePix = &keepRatioScaledPix;
}

// 提前各个区域图片
QPixmap pix1 = handlePix->copy(0, 0, pos1Width, pos1Height);
QPixmap pix2 = handlePix->copy(pos1Width, 0, qMax(handlePixW - pos1Width - pos3Width, 0), pos1Height);
QPixmap pix3 = handlePix->copy(qMax(handlePixW - pos3Width, 0), 0, pos3Width, pos1Height);
QPixmap pix4 = handlePix->copy(0, pos1Height, pos1Width, qMax(handlePixH - pos1Height - pos7Height, 0));
QPixmap pix5 = handlePix->copy(pos1Width, pos1Height, qMax(handlePixW - pos1Width - pos3Width, 0), qMax(handlePixH - pos1Height - pos7Height, 0));
QPixmap pix6 = handlePix->copy(qMax(handlePixW - pos3Width, 0), pos1Height, pos3Width, qMax(handlePixH - pos1Height - pos7Height, 0));
QPixmap pix7 = handlePix->copy(0, qMax(handlePixH - pos7Height, 0), pos1Width, pos7Height);
QPixmap pix8 = handlePix->copy(pos1Width, qMax(handlePixH - pos7Height, 0), qMax(handlePixW - pos1Width - pos3Width, 0), pos7Height);
QPixmap pix9 = handlePix->copy(qMax(handlePixW - pos3Width, 0), qMax(handlePixH - pos7Height, 0), pos3Width, pos7Height);

// 对2、4、5、6、8区域图片进行缩放
pix2 = pix2.scaled(qMax(destSize.width() - pos1Width - pos3Width, 0), pos1Height, Qt::IgnoreAspectRatio, mode);
pix4 = pix4.scaled(pos1Width, qMax(destSize.height() - pos1Height - pos7Height, 0), Qt::IgnoreAspectRatio, mode);
pix5 = pix5.scaled(qMax(destSize.width() - pos1Width - pos3Width, 0), qMax(destSize.height() - pos1Height - pos7Height, 0), Qt::IgnoreAspectRatio, mode);
pix6 = pix6.scaled(pos3Width, qMax(destSize.height() - pos1Height - pos7Height, 0), Qt::IgnoreAspectRatio, mode);
pix8 = pix8.scaled(qMax(destSize.width() - pos1Width - pos3Width, 0), pos7Height, Qt::IgnoreAspectRatio, mode);

// 拼接新的图片
QPixmap dest(destSize);
dest.fill(Qt::transparent); // 使用透明色填充图片

QPainter painter(&dest);

painter.drawPixmap(0, 0, pix1);
painter.drawPixmap(pos1Width, 0, pix2);
painter.drawPixmap(qMax(destSize.width() - pos3Width, 0), 0, pix3);
painter.drawPixmap(0, pos1Height, pix4);
painter.drawPixmap(pos1Width, pos1Height, pix5);
painter.drawPixmap(qMax(destSize.width() - pos3Width, 0), pos1Height, pix6);
painter.drawPixmap(0, qMax(destSize.height() - pos7Height, 0), pix7);
painter.drawPixmap(pos1Width, qMax(destSize.height() - pos7Height, 0), pix8);
painter.drawPixmap(qMax(destSize.width() - pos3Width, 0), qMax(destSize.height() - pos7Height, 0), pix9);
painter.end();

return dest;
}

调试小技巧:

可以把每个区域的图片保存到本地,查看图片是否正确,如:

1
2
3
4
5
6
7
8
9
pix1.save("D:\\1.png");
pix2.save("D:\\2.png");
pix3.save("D:\\3.png");
pix4.save("D:\\4.png");
pix5.save("D:\\5.png");
pix6.save("D:\\6.png");
pix7.save("D:\\7.png");
pix8.save("D:\\8.png");
pix9.save("D:\\9.png");


限于政策原因,在您看到该文章时,博客可能已经关闭了评论功能🥺

您可以通过在 blog-comment 项目中提交Issue来间接地发表评论🍀