diff --git a/assets/icons/icons.qrc b/assets/icons/icons.qrc
index 6311349ce..25e02b646 100644
--- a/assets/icons/icons.qrc
+++ b/assets/icons/icons.qrc
@@ -16,6 +16,14 @@
texts/ocr-normal_32px.svg
texts/table-recognition_32px.svg
texts/sequence_32px.svg
+ texts/corner-radius_32px.svg
+ texts/corner-sharp_32px.svg
+ texts/corner-adjust_32px.svg
+ texts/size-ratio_32px.svg
+ texts/ratio-free_32px.svg
+ texts/ratio-lock_32px.svg
+ texts/ratio-unlock_32px.svg
+ texts/selection-fixed_32px.svg
texts/oval-normal_32px.svg
texts/pinscreenshots_32px.svg
texts/recorder_32px.svg
diff --git a/assets/icons/texts/corner-adjust_32px.svg b/assets/icons/texts/corner-adjust_32px.svg
new file mode 100644
index 000000000..1d40294c4
--- /dev/null
+++ b/assets/icons/texts/corner-adjust_32px.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/assets/icons/texts/corner-radius_32px.svg b/assets/icons/texts/corner-radius_32px.svg
new file mode 100644
index 000000000..c65deba5e
--- /dev/null
+++ b/assets/icons/texts/corner-radius_32px.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/assets/icons/texts/corner-sharp_32px.svg b/assets/icons/texts/corner-sharp_32px.svg
new file mode 100644
index 000000000..bd9f1ca24
--- /dev/null
+++ b/assets/icons/texts/corner-sharp_32px.svg
@@ -0,0 +1,9 @@
+
+
diff --git a/assets/icons/texts/ratio-free_32px.svg b/assets/icons/texts/ratio-free_32px.svg
new file mode 100644
index 000000000..3520977eb
--- /dev/null
+++ b/assets/icons/texts/ratio-free_32px.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/assets/icons/texts/ratio-lock_32px.svg b/assets/icons/texts/ratio-lock_32px.svg
new file mode 100644
index 000000000..33b880577
--- /dev/null
+++ b/assets/icons/texts/ratio-lock_32px.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/assets/icons/texts/ratio-unlock_32px.svg b/assets/icons/texts/ratio-unlock_32px.svg
new file mode 100644
index 000000000..d7a3f7b15
--- /dev/null
+++ b/assets/icons/texts/ratio-unlock_32px.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/assets/icons/texts/selection-fixed_32px.svg b/assets/icons/texts/selection-fixed_32px.svg
new file mode 100644
index 000000000..f28dd97ec
--- /dev/null
+++ b/assets/icons/texts/selection-fixed_32px.svg
@@ -0,0 +1,10 @@
+
diff --git a/assets/icons/texts/size-ratio_32px.svg b/assets/icons/texts/size-ratio_32px.svg
new file mode 100644
index 000000000..3520977eb
--- /dev/null
+++ b/assets/icons/texts/size-ratio_32px.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/src/main_window.cpp b/src/main_window.cpp
index 77e6247ab..b80a8d607 100755
--- a/src/main_window.cpp
+++ b/src/main_window.cpp
@@ -49,6 +49,7 @@ extern "C" {
#include
#include
#include
+#include
#include
#include
#include
@@ -59,6 +60,7 @@ extern "C" {
#include
#include
#include
+#include
#include
#include
#include
@@ -521,6 +523,41 @@ void MainWindow::initAttributes()
m_sideBar = new SideBar(this);
m_sideBar->hide();
+ // 选区右上角的圆角 / 尺寸比例工具组
+ m_roundedCornerWidget = new RoundedCornerWidget(this);
+ m_roundedCornerWidget->hide();
+ connect(m_roundedCornerWidget, &RoundedCornerWidget::radiusChanged, this, [this](int radius) {
+ // 圆角实时预览只影响选区四个角,按角区域局部重绘即可。
+ // 直接 update() 会整屏重绘(背景位图 + 蒙层),拖动滑块时会明显卡顿。
+ if (status::shot != m_functionType || recordWidth <= 0 || recordHeight <= 0) {
+ m_radiusPreviewRadius = radius;
+ update();
+ return;
+ }
+ // 同时覆盖旧值和新值影响的范围,半径变小时旧圆角能正确恢复
+ const int span = qMax(m_radiusPreviewRadius, radius);
+ m_radiusPreviewRadius = radius;
+ QRegion dirty;
+ if (span > 0) {
+ const int side = span + 2; // +1 抗锯齿边界,+1 虚线框
+ const int left = recordX - 1;
+ const int right = recordX + recordWidth - span - 1;
+ const int top = recordY - 1;
+ const int bottom = recordY + recordHeight - span - 1;
+ dirty += QRect(left, top, side, side);
+ dirty += QRect(right, top, side, side);
+ dirty += QRect(left, bottom, side, side);
+ dirty += QRect(right, bottom, side, side);
+ }
+ if (dirty.isEmpty()) {
+ update();
+ } else {
+ update(dirty);
+ }
+ });
+ connect(m_roundedCornerWidget, &RoundedCornerWidget::selectionSizeChanged,
+ this, &MainWindow::onRoundedSelectionSizeChanged);
+
m_sizeTips = new TopTips(this);
m_sizeTips->setFullScreenRecord(m_isFullScreenRecord);
m_sizeTips->hide();
@@ -1315,12 +1352,53 @@ void MainWindow::initToolBarShortcut()
qCDebug(dsrApp) << "initShortcut end";
}
+bool MainWindow::isRoundedPanelEditorFocused() const
+{
+ QWidget *focus = QApplication::focusWidget();
+ if (!focus || !m_roundedCornerWidget) {
+ return false;
+ }
+ // 只对文本输入控件让路:滑块、按钮上的回车仍然保持“保存截图”的含义。
+ if (!qobject_cast(focus) && !qobject_cast(focus)) {
+ return false;
+ }
+ for (QWidget *widget = focus; widget; widget = widget->parentWidget()) {
+ if (widget == m_roundedCornerWidget) {
+ return true;
+ }
+ }
+ return false;
+}
+
void MainWindow::initSaveShortcut()
{
+ // 重复初始化时先清掉旧的快捷方式,避免残留的 QShortcut 继续抢占回车
+ delete m_returnSaveShortcut;
+ delete m_enterSaveShortcut;
+ m_returnSaveShortcut = nullptr;
+ m_enterSaveShortcut = nullptr;
+
// 截图模式/滚动模式 保存截图 大键盘的enter
QShortcut *returnSC = new QShortcut(QKeySequence(Qt::Key_Return), this);
// 截图模式/滚动模式 保存截图 小键盘的enter
QShortcut *enterSC = new QShortcut(QKeySequence(Qt::Key_Enter), this);
+ m_returnSaveShortcut = returnSC;
+ m_enterSaveShortcut = enterSC;
+
+ // QShortcut 会先于焦点控件拿到按键:焦点在圆角/尺寸比例面板的输入框里时,
+ // 回车属于“确认输入”,必须先把保存截图的快捷方式禁用掉,输入框才能收到回车。
+ auto updateSaveShortcutEnabled = [this]() {
+ const bool editing = isRoundedPanelEditorFocused();
+ if (m_returnSaveShortcut) {
+ m_returnSaveShortcut->setEnabled(!editing);
+ }
+ if (m_enterSaveShortcut) {
+ m_enterSaveShortcut->setEnabled(!editing);
+ }
+ };
+ connect(qApp, &QApplication::focusChanged, this, [updateSaveShortcutEnabled](QWidget *, QWidget *) {
+ updateSaveShortcutEnabled();
+ });
// 截图模式/滚动模式 保存截图
QShortcut *saveShotSC = new QShortcut(QKeySequence("Ctrl+S"), this);
// 截图模式/录屏模式(未做穿透)/滚动模式 退出
@@ -2723,7 +2801,17 @@ bool MainWindow::saveImg(const QPixmap &pix, const QString &fileName, const char
}
if (status::pinscreenshots == m_functionType)
return false;
- if (pix.save(fileName, format, quality)) {
+
+ // 圆角截图:JPG/BMP 不支持透明,圆角外按设计稿填充白色。
+ // 此处只做「白底合成」,不能再裁一次圆角,否则会把已经生成的投影边缘裁掉。
+ // 不读取配置里的 radius:滑块落盘有防抖,保存时配置可能仍停留在 0;
+ // 以图像实际 alpha 为准,才能保证透明圆角不会在 JPEG/BMP 中被编码成黑角。
+ QPixmap savePix = pix;
+ const QString fmt = QString::fromLatin1(format).toUpper();
+ if (fmt != QLatin1String("PNG") && pix.hasAlphaChannel()) {
+ savePix = RoundedCornerWidget::compositeOnWhite(pix);
+ }
+ if (savePix.save(fileName, format, quality)) {
qCInfo(dsrApp) << __FUNCTION__ << __LINE__ << "保存图片成功!保存质量: " << quality;
return true;
} else {
@@ -3367,6 +3455,26 @@ void MainWindow::updateToolBarPos()
// TODO: treeland适配,解决问题临时方案
if (!(Utils::isTreelandMode))
m_toolBar->showAt(toolbarPoint);
+
+ // 圆角 / 尺寸比例工具组跟随选区右上角
+ if (m_roundedCornerWidget) {
+ const bool canShow = (status::shot == m_functionType)
+ && !Utils::isTreelandMode
+ && recordWidth > 0 && recordHeight > 0;
+ if (canShow) {
+ // 尺寸面板与左上角尺寸提示保持一致,展示实际截图像素大小
+ const qreal pixelRatio = qApp->primaryScreen()->devicePixelRatio();
+ m_roundedCornerWidget->setSelectionSize(
+ qMax(1, qRound((recordWidth - 2) * pixelRatio)),
+ qMax(1, qRound((recordHeight - 1) * pixelRatio)));
+ m_roundedCornerWidget->followSelection(
+ QRect(recordX, recordY, recordWidth, recordHeight), m_backgroundRect);
+ m_roundedCornerWidget->show();
+ m_roundedCornerWidget->raise();
+ } else {
+ m_roundedCornerWidget->hide();
+ }
+ }
}
void MainWindow::updateSideBarPos()
@@ -3965,6 +4073,10 @@ void MainWindow::changeShotToolEvent(const QString &func)
qCDebug(dsrApp) << "MainWindow::changeShotToolEvent >> func: " << func;
+ // 单击其它工具图标时收起圆角/尺寸比例属性面板
+ if (m_roundedCornerWidget) {
+ m_roundedCornerWidget->collapsePanels();
+ }
// 调用ocr功能时先截图后,退出截图录屏,将刚截图的图片串递到ocr识别界面;
if (func == "ocr") {
{
@@ -5187,13 +5299,40 @@ void MainWindow::paintEvent(QPaintEvent *event)
painter.setBrush(QBrush()); // clear brush
painter.setPen(framePen);
- // 计算虚线绘制的实际区域
- // 修复:让虚线边界与实际选中区域完全一致
- // 移除不必要的调整,确保虚线显示的就是实际选中的区域
-
- // 直接使用实际选中区域绘制虚线,确保一致性
- painter.drawRect(QRect(recordX, recordY, recordWidth, recordHeight));
+ // QPainter::drawRect(QRect(...)) 在 1px 画笔下会把上/左边画在选区内部的第一个像素,
+ // 内容为白色时虚线会完全融进背景;下/右边则落在选区外,因此视觉上只剩两条边。
+ // 默认将虚线框整体外移 1px 放到蒙层上;选区贴到屏幕边缘时不能再外移,
+ // 否则边缘像素会被窗口裁掉,因此按绘制区域夹取四个边界。
+ const QRect windowRect = rect();
+ const int frameLeft = qMax(recordX - 1, windowRect.left());
+ const int frameTop = qMax(recordY - 1, windowRect.top());
+ const int frameRight = qMin(recordX + recordWidth, windowRect.right());
+ const int frameBottom = qMin(recordY + recordHeight, windowRect.bottom());
+ // QPainter::drawRect(QRect) 的右/下边界按 x + width / y + height 落点,
+ // 因此这里使用 right-left / bottom-top,保证最后一行、最后一列不被裁掉。
+ painter.drawRect(QRect(frameLeft, frameTop,
+ frameRight - frameLeft,
+ frameBottom - frameTop));
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ }
+
+ // 圆角实时预览:把选中区域四个圆角外重新覆盖半透明蒙层,使其呈现圆角
+ if (m_roundedCornerWidget && m_roundedCornerWidget->radius() > 0
+ && status::shot == m_functionType && status::scrollshot != m_functionType) {
+ const qreal r = static_cast(m_roundedCornerWidget->radius());
+ QPainterPath roundedPath;
+ roundedPath.addRoundedRect(QRectF(frameRect), r, r);
+ QPainterPath framePath;
+ framePath.addRect(QRectF(frameRect));
+ const QPainterPath outside = framePath.subtracted(roundedPath);
+ painter.save();
painter.setRenderHint(QPainter::Antialiasing, true);
+ painter.setOpacity(0.2);
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(QBrush("#000000"));
+ painter.setClipRegion(QRegion(backgroundRect));
+ painter.drawPath(outside);
+ painter.restore();
}
// Draw drag pint.
@@ -5357,6 +5496,10 @@ int MainWindow::mousePressEF(QMouseEvent *mouseEvent, bool &needRepaint)
dragRecordY = recordY;
dragRecordWidth = recordWidth;
dragRecordHeight = recordHeight;
+ // 记录锁定比例基准,供拖拽过程中等比缩放使用
+ m_lockedRatio = (recordWidth > 0 && recordHeight > 0)
+ ? static_cast(recordWidth) / static_cast(recordHeight)
+ : 0.0;
if (recordButtonStatus == RECORD_BUTTON_NORMAL) {
// hideRecordButton();
@@ -5445,6 +5588,10 @@ int MainWindow::mouseReleaseEF(QMouseEvent *mouseEvent, bool &needRepaint)
}
if (recordX < 0) recordX = 0;
if (recordY < 0) recordY = 0;
+
+ // 图钉开启时,本次启动的第一次框选完成后再套用记忆值。
+ // 放在释放阶段可以拿到最终选区,也能避免拖拽过程中反复纠正。
+ applyRememberedSelectionSetting();
}
// 尺寸调整完成后更新提示和工具栏位置
@@ -5617,6 +5764,9 @@ int MainWindow::mouseMoveEF(QMouseEvent *mouseEvent, bool &needRepaint)
resizeRight(mouseEvent);
}
+ // 锁定比例时,拖拽后按固定宽高比等比修正选区
+ applyLockedRatioToSelection();
+
needRepaint = true;
}
}
@@ -5859,8 +6009,15 @@ int MainWindow::keyPressEF(QKeyEvent *keyEvent, bool &needRepaint)
}
if (m_shotStatus == ShotMouseStatus::Normal) {
+ // 锁定比例后禁用单边扩大/缩小快捷键(Ctrl/Shift+Ctrl + 方向键)
+ const bool ratioLocked = m_roundedCornerWidget && m_roundedCornerWidget->isRatioLocked();
+ const bool resizeShortcut = keyEvent->key() == Qt::Key_Left || keyEvent->key() == Qt::Key_Right
+ || keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down;
// 是否按住 shift+ctrl
if (keyEvent->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) {
+ if (ratioLocked && resizeShortcut) {
+ return 0;
+ }
if (keyEvent->key() == Qt::Key_Left) {
if (recordWidth > RECORD_MIN_SHOT_SIZE) {
recordX = std::max(0, recordX + 1);
@@ -5893,6 +6050,9 @@ int MainWindow::keyPressEF(QKeyEvent *keyEvent, bool &needRepaint)
}
// 是否只按住 ctrl
else if ((qApp->keyboardModifiers() == Qt::ControlModifier)) {
+ if (ratioLocked && resizeShortcut) {
+ return 0;
+ }
if (keyEvent->key() == Qt::Key_S) {
// expressSaveScreenshot();
saveScreenShot();
@@ -7280,8 +7440,20 @@ void MainWindow::shotCurrentImg()
m_resultPixmap = paintImage();
}
- m_resultPixmap = ImageBorderHelper::instance()->getPixmapAddBorder(m_resultPixmap);
- addCursorToImage();
+ const int roundedRadius = m_roundedCornerWidget ? m_roundedCornerWidget->radius() : 0;
+ // 圆角属于截图成像效果:普通截图与「贴图」都需要带上圆角,
+ // OCR / 表格识别 / 滚动截图需要的是原始像素,不做圆角裁切。
+ const bool roundableMode = (status::shot == m_functionType
+ || status::pinscreenshots == m_functionType);
+ if (roundedRadius > 0 && roundableMode) {
+ // 顺序:标注 -> 光标 -> 圆角裁切 -> 投影
+ addCursorToImage();
+ m_resultPixmap = RoundedCornerWidget::applyRoundedCorners(m_resultPixmap, roundedRadius);
+ m_resultPixmap = ImageBorderHelper::instance()->getPixmapAddBorder(m_resultPixmap);
+ } else {
+ m_resultPixmap = ImageBorderHelper::instance()->getPixmapAddBorder(m_resultPixmap);
+ addCursorToImage();
+ }
qCInfo(dsrApp) << __FUNCTION__ << __LINE__ << "已截取当前图片!";
#endif
}
@@ -7874,6 +8046,9 @@ void MainWindow::hideAllWidget()
m_toolBar->hide();
m_sideBar->hide();
}
+ if (m_roundedCornerWidget) {
+ m_roundedCornerWidget->hide();
+ }
// m_toolBar->hide();
// m_sideBar->hide();
@@ -8040,6 +8215,151 @@ void MainWindow::reloadImage(QString effect, int radius)
#endif
}
+void MainWindow::applyRememberedSelectionSetting()
+{
+ if (status::shot != m_functionType || !m_roundedCornerWidget
+ || !m_roundedCornerWidget->hasPendingRememberedSetting()) {
+ return;
+ }
+
+ const QSize setting = m_roundedCornerWidget->rememberedSetting();
+ if (setting.width() <= 0 || setting.height() <= 0) {
+ m_roundedCornerWidget->consumeRememberedSetting();
+ return;
+ }
+
+ const int minWidth = qMax(1, RECORD_MIN_SHOT_SIZE);
+ const int minHeight = qMax(1, RECORD_MIN_SHOT_SIZE);
+ const int maxWidth = qMax(minWidth, m_backgroundRect.width());
+ const int maxHeight = qMax(minHeight, m_backgroundRect.height());
+
+ int newWidth = recordWidth;
+ int newHeight = recordHeight;
+ if (m_roundedCornerWidget->isRememberedSettingRatio()) {
+ const qreal ratio = static_cast(setting.width()) / static_cast(setting.height());
+ // 选区宽高到最终截图还会减去 2/1 像素边框;这里先在最终输出像素上套用比例,
+ // 否则保存出来的图片比例会因边框补偿产生偏差。
+ const int minOutputWidth = qMax(1, minWidth - 2);
+ const int minOutputHeight = qMax(1, minHeight - 1);
+ const int maxOutputWidth = qMax(minOutputWidth, maxWidth - 2);
+ const int maxOutputHeight = qMax(minOutputHeight, maxHeight - 1);
+ int outputWidth = qMax(minOutputWidth, recordWidth - 2);
+ int outputHeight = qMax(minOutputHeight, qRound(outputWidth / ratio));
+
+ if (outputHeight > maxOutputHeight) {
+ outputHeight = maxOutputHeight;
+ outputWidth = qMax(minOutputWidth, qRound(outputHeight * ratio));
+ }
+ if (outputWidth > maxOutputWidth) {
+ outputWidth = maxOutputWidth;
+ outputHeight = qMax(minOutputHeight, qRound(outputWidth / ratio));
+ }
+
+ newWidth = outputWidth + 2;
+ newHeight = outputHeight + 1;
+ } else {
+ const qreal pixelRatio = qApp->primaryScreen()->devicePixelRatio() > 0
+ ? qApp->primaryScreen()->devicePixelRatio() : 1.0;
+ newWidth = qRound(setting.width() / pixelRatio) + 2;
+ newHeight = qRound(setting.height() / pixelRatio) + 1;
+ newWidth = qBound(minWidth, newWidth, maxWidth);
+ newHeight = qBound(minHeight, newHeight, maxHeight);
+ }
+
+ recordWidth = qBound(minWidth, newWidth, maxWidth);
+ recordHeight = qBound(minHeight, newHeight, maxHeight);
+ recordX = qBound(0, recordX, qMax(0, m_backgroundRect.width() - recordWidth));
+ recordY = qBound(0, recordY, qMax(0, m_backgroundRect.height() - recordHeight));
+ m_lockedRatio = static_cast(recordWidth) / static_cast(recordHeight);
+ m_roundedCornerWidget->consumeRememberedSetting();
+
+ qCDebug(dsrApp) << "apply remembered selection setting:"
+ << setting << "ratio:" << m_roundedCornerWidget->isRememberedSettingRatio()
+ << "selection:" << QRect(recordX, recordY, recordWidth, recordHeight);
+}
+
+void MainWindow::onRoundedSelectionSizeChanged(int width, int height)
+{
+ if (status::shot != m_functionType) {
+ return;
+ }
+ const qreal pixelRatio = qApp->primaryScreen()->devicePixelRatio() > 0
+ ? qApp->primaryScreen()->devicePixelRatio() : 1.0;
+ // 尺寸面板与实际截图裁剪保持一致:宽-2、高-1,并限制不超出屏幕
+ const int maxWidth = qMax(RECORD_MIN_SHOT_SIZE, m_backgroundRect.width() - recordX - 2);
+ const int maxHeight = qMax(RECORD_MIN_SHOT_SIZE, m_backgroundRect.height() - recordY - 1);
+ const int newWidth = qBound(RECORD_MIN_SHOT_SIZE, qRound(width / pixelRatio) + 2, maxWidth);
+ const int newHeight = qBound(RECORD_MIN_SHOT_SIZE, qRound(height / pixelRatio) + 1, maxHeight);
+ if (newWidth == recordWidth && newHeight == recordHeight) {
+ return;
+ }
+ recordWidth = newWidth;
+ recordHeight = newHeight;
+ if (m_sizeTips) {
+ m_sizeTips->updateTips(QPoint(recordX, recordY), QSize(recordWidth, recordHeight));
+ }
+ updateToolBarPos();
+ if (m_sideBar && m_sideBar->isVisible()) {
+ updateSideBarPos();
+ }
+ if (m_cameraWidget && m_cameraWidget->isVisible()) {
+ updateCameraWidgetPos();
+ }
+ update();
+}
+
+void MainWindow::applyLockedRatioToSelection()
+{
+ if (status::shot != m_functionType || dragAction == ACTION_MOVE) {
+ return;
+ }
+ if (!m_roundedCornerWidget || !m_roundedCornerWidget->isRatioLocked() || m_lockedRatio <= 0.0) {
+ return;
+ }
+
+ // 固定比例后,以变化幅度较大的一边作为驱动边,另一边按比例联动
+ const qreal wScale = dragRecordWidth > 0
+ ? static_cast(recordWidth) / static_cast(dragRecordWidth) : 1.0;
+ const qreal hScale = dragRecordHeight > 0
+ ? static_cast(recordHeight) / static_cast(dragRecordHeight) : 1.0;
+ const bool widthDriven = qAbs(wScale - 1.0) >= qAbs(hScale - 1.0);
+
+ const bool leftEdge = (dragAction == ACTION_RESIZE_LEFT
+ || dragAction == ACTION_RESIZE_TOP_LEFT
+ || dragAction == ACTION_RESIZE_BOTTOM_LEFT);
+ const bool topEdge = (dragAction == ACTION_RESIZE_TOP
+ || dragAction == ACTION_RESIZE_TOP_LEFT
+ || dragAction == ACTION_RESIZE_TOP_RIGHT);
+
+ // 保持对边(单边拖拽)或对角(角点拖拽)固定
+ const int anchorRight = dragRecordX + dragRecordWidth;
+ const int anchorBottom = dragRecordY + dragRecordHeight;
+ const int maxWidth = leftEdge ? anchorRight : (m_backgroundRect.width() - dragRecordX);
+ const int maxHeight = topEdge ? anchorBottom : (m_backgroundRect.height() - dragRecordY);
+
+ int newWidth = qMax(RECORD_MIN_SHOT_SIZE, recordWidth);
+ int newHeight = qMax(RECORD_MIN_SHOT_SIZE, recordHeight);
+ if (widthDriven) {
+ newHeight = qMax(RECORD_MIN_SHOT_SIZE, qRound(newWidth / m_lockedRatio));
+ } else {
+ newWidth = qMax(RECORD_MIN_SHOT_SIZE, qRound(newHeight * m_lockedRatio));
+ }
+
+ // 限制在屏幕可用范围内,受限时按比例回退
+ newWidth = qMin(newWidth, qMax(RECORD_MIN_SHOT_SIZE, maxWidth));
+ newHeight = qMax(RECORD_MIN_SHOT_SIZE, qRound(newWidth / m_lockedRatio));
+ if (newHeight > qMax(RECORD_MIN_SHOT_SIZE, maxHeight)) {
+ newHeight = qMax(RECORD_MIN_SHOT_SIZE, maxHeight);
+ newWidth = qMax(RECORD_MIN_SHOT_SIZE, qRound(newHeight * m_lockedRatio));
+ newWidth = qMin(newWidth, qMax(RECORD_MIN_SHOT_SIZE, maxWidth));
+ }
+
+ recordWidth = newWidth;
+ recordHeight = newHeight;
+ recordX = qMax(0, leftEdge ? (anchorRight - newWidth) : dragRecordX);
+ recordY = qMax(0, topEdge ? (anchorBottom - newHeight) : dragRecordY);
+}
+
void MainWindow::shotImgWidthEffect()
{
#ifndef ENABLE_UNIT_TEST
diff --git a/src/main_window.h b/src/main_window.h
index 0e7170b0d..fa0ebf088 100755
--- a/src/main_window.h
+++ b/src/main_window.h
@@ -19,8 +19,13 @@
#include "widgets/shapeswidget.h"
#include "widgets/toptips.h"
#include
+
+QT_BEGIN_NAMESPACE
+class QShortcut;
+QT_END_NAMESPACE
#include "widgets/toolbar.h"
#include "widgets/sidebar.h"
+#include "widgets/roundedcornerwidget.h"
#include "widgets/keybuttonwidget.h"
#include "widgets/zoomIndicator.h"
#include "widgets/camerawidget.h"
@@ -1283,6 +1288,33 @@ public slots:
int m_screenWidth = 0; //屏幕宽度
int m_screenHeight = 0; //屏幕高度
SideBar *m_sideBar = nullptr; //截图功能侧边栏功能
+ RoundedCornerWidget *m_roundedCornerWidget = nullptr; //选区右上角圆角/尺寸比例工具组
+ qreal m_lockedRatio = 0.0; //拖拽开始时记录的选区宽高比(锁定比例用)
+ int m_radiusPreviewRadius = 0; //上一次圆角预览半径,用于计算需要重绘的四角区域
+ // 保存截图的回车快捷方式:焦点在圆角/尺寸比例面板的输入框时要临时禁用,
+ // 否则 QShortcut 会先于输入框吃掉回车,导致“输入尺寸回车直接结束截图”。
+ QShortcut *m_returnSaveShortcut = nullptr;
+ QShortcut *m_enterSaveShortcut = nullptr;
+
+ /**
+ * @brief 当前焦点是否位于圆角/尺寸比例面板的文本输入框内
+ */
+ bool isRoundedPanelEditorFocused() const;
+
+ /**
+ * @brief 圆角工具组尺寸/比例面板修改了选区尺寸
+ */
+ void onRoundedSelectionSizeChanged(int width, int height);
+
+ /**
+ * @brief 应用上次勾选“下次截图保持”的尺寸或比例
+ */
+ void applyRememberedSelectionSetting();
+
+ /**
+ * @brief 锁定比例后,拖拽选区时按固定宽高比等比调整
+ */
+ void applyLockedRatioToSelection();
/**
* @brief 在一级工具栏按下鼠标左键的时,此时记录下二级菜单的位置
*/
diff --git a/src/src.pro b/src/src.pro
index 97f55a9d6..489247d02 100755
--- a/src/src.pro
+++ b/src/src.pro
@@ -203,6 +203,7 @@ HEADERS += main_window.h \
widgets/keybuttonwidget.h \
widgets/sidebar.h \
widgets/shottoolwidget.h \
+ widgets/roundedcornerwidget.h \
widgets/colortoolwidget.h \
widgets/shapetoolwidget.h \
widgets/savebutton.h \
@@ -298,6 +299,7 @@ SOURCES += main.cpp \
widgets/keybuttonwidget.cpp \
widgets/sidebar.cpp \
widgets/shottoolwidget.cpp \
+ widgets/roundedcornerwidget.cpp \
widgets/colortoolwidget.cpp \
widgets/shapetoolwidget.cpp \
widgets/savebutton.cpp \
diff --git a/src/utils/configsettings.cpp b/src/utils/configsettings.cpp
index 167cd7e0e..9b38d2747 100755
--- a/src/utils/configsettings.cpp
+++ b/src/utils/configsettings.cpp
@@ -36,6 +36,9 @@ const QMap> ConfigSettings::m_defaultConfig = {
{"text", {{"color_index", 3}, {"fontsize", 0}}},
// 序号标注工具,颜色与线宽(线宽对应序号圆圈大小档位)
{"sequence", {{"color_index", 3}, {"line_width", 1}}},
+ // 圆角截图:开启状态由 radius>0 表示;ratio_type 表示尺寸/比例下拉模式,
+ // remember 表示下次启动是否应用 fixed_width/fixed_height。
+ {"rounded", {{"radius", 0}, {"ratio_type", 0}, {"remember", false}, {"remember_ratio", false}, {"fixed_width", 0}, {"fixed_height", 0}}},
// 几何图形类型,矩形或椭圆
{"shape", {{"current", "rectangle"}}},
// 截图保存选项,格式,保存位置选项
diff --git a/src/widgets/roundedcornerwidget.cpp b/src/widgets/roundedcornerwidget.cpp
new file mode 100644
index 000000000..f68cfb80d
--- /dev/null
+++ b/src/widgets/roundedcornerwidget.cpp
@@ -0,0 +1,1027 @@
+// Copyright (C) 2020 ~ 2021 Uniontech Software Technology Co.,Ltd.
+// SPDX-FileCopyrightText: 2022-2026 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "roundedcornerwidget.h"
+#include "toolbutton.h"
+#include "../utils/configsettings.h"
+#include "../utils.h"
+#include "../utils/log.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+DWIDGET_USE_NAMESPACE
+
+namespace {
+
+// 设计稿「截图录屏-工具栏-圆角 / 比例」尺寸(24x24 图标按 32px 资源提供,控件按 1x 逻辑像素实现)
+const int GROUP_WIDTH = 56;
+const int GROUP_HEIGHT = 103;
+const int BUTTON_SIZE = 36;
+const int BUTTON_ICON_SIZE = 24;
+const int BUTTON_MARGIN_X = 10;
+const int FIRST_BUTTON_Y = 11;
+const int SECOND_BUTTON_Y = 57;
+
+const int PANEL_WIDTH = 270;
+const int PANEL_HEIGHT = 56;
+const int PANEL_RADIUS = 12;
+const int PANEL_PADDING = 10;
+
+const int SLIDER_X = 36;
+const int SLIDER_Y = 16;
+const int SLIDER_WIDTH = 152;
+const int SLIDER_HEIGHT = 24;
+const int SPIN_WIDTH = 62;
+const int SPIN_HEIGHT = 36;
+const int ADJUST_ICON_SIZE = 16;
+
+const int SIZE_MODE_X = 10;
+const int SIZE_MODE_Y = 16;
+const int SIZE_MODE_WIDTH = 55;
+const int SIZE_MODE_HEIGHT = 24;
+const int WIDTH_SPIN_X = 74;
+const int HEIGHT_SPIN_X = 154;
+const int DIMENSION_SPIN_WIDTH = 60;
+const int DIMENSION_SEPARATOR_X = 136;
+const int DIMENSION_SEPARATOR_Y = 19;
+const int DIMENSION_SEPARATOR_SIZE = 16;
+const int LOCK_BUTTON_X = 224;
+
+const int RADIUS_MAX = 60;
+const int MAX_DIMENSION_VALUE = 99999;
+const int MAX_RATIO_VALUE = 999;
+// 按钮组与选区、按钮组与属性面板的间距
+const int SELECTION_GAP = 16;
+const int PANEL_GAP = 10;
+
+// DTK 默认 DSlider 的滑块是蓝色胶囊;设计稿要求 16x16、圆角 4px 的深色方形滑块。
+// 轨道仍由当前 DTK 样式绘制,只替换 handle,避免重新实现整套 slider 行为。
+class RoundedSliderStyle : public QProxyStyle
+{
+public:
+ explicit RoundedSliderStyle(QObject *parent)
+ // 这里必须传 nullptr 基样式,不能传控件当前样式:
+ // QProxyStyle 会把基样式收成自己的子对象,而 QSlider 默认拿到的是
+ // QApplication 的应用样式;一旦把应用样式传进来,slider 析构时就会
+ // 连带销毁应用样式,QApplicationPrivate::app_style 变成悬空指针,
+ // 退出时 ~QApplication 再次释放它就会崩溃。
+ // 传 nullptr 时 QProxyStyle::baseStyle() 会在绘制时动态取回应用样式。
+ : QProxyStyle(nullptr)
+ {
+ setParent(parent);
+ }
+
+ void drawComplexControl(ComplexControl control, const QStyleOptionComplex *option,
+ QPainter *painter, const QWidget *widget) const override
+ {
+ if (control != QStyle::CC_Slider || !option || !painter) {
+ QProxyStyle::drawComplexControl(control, option, painter, widget);
+ return;
+ }
+
+ const auto *sliderOption = qstyleoption_cast(option);
+ if (!sliderOption) {
+ QProxyStyle::drawComplexControl(control, option, painter, widget);
+ return;
+ }
+
+ const bool darkTheme = DGuiApplicationHelper::instance()->themeType()
+ == DGuiApplicationHelper::DarkType;
+ constexpr qreal HANDLE_SIZE = 16.0;
+ constexpr qreal HANDLE_RADIUS = 4.0;
+
+ // 轨道范围用 groove(也就是可滑动区间),不要用 defaultHandle 的圆心定位:
+ // DTK 原生手柄约 28px,设计稿手柄是 16px,两者圆心相同但宽度不同,
+ // 直接套用圆心会把 16px 手柄整体往右顶 6px,最小值时轨道会在手柄左边露出一截。
+ QRectF trackRect(QProxyStyle::subControlRect(
+ control, sliderOption, QStyle::SC_SliderGroove, widget));
+ if (!trackRect.isValid()) {
+ trackRect = QRectF(sliderOption->rect);
+ }
+ if (trackRect.width() < HANDLE_SIZE) {
+ trackRect = QRectF(sliderOption->rect);
+ }
+
+ const int range = sliderOption->maximum - sliderOption->minimum;
+ const qreal fraction = range > 0
+ ? qreal(sliderOption->sliderPosition - sliderOption->minimum) / range
+ : 0.0;
+ // 最小值时手柄左边缘贴住轨道左端,最大值时右边缘贴住轨道右端,
+ // 这样手柄两端各占 16px,中间正好留出设计稿的 120px 虚线区间。
+ QRectF handleRect(0, 0, HANDLE_SIZE, HANDLE_SIZE);
+ handleRect.moveTopLeft(QPointF(trackRect.left() + fraction * (trackRect.width() - HANDLE_SIZE),
+ trackRect.center().y() - HANDLE_SIZE / 2.0));
+
+ const qreal trackY = trackRect.center().y();
+ const QColor trackColor = darkTheme ? QColor(255, 255, 255, 51)
+ : QColor(0, 0, 0, 51);
+ const QColor progressColor = darkTheme ? QColor(2, 76, 202)
+ : QColor(0, 91, 255);
+
+ painter->save();
+ painter->setRenderHint(QPainter::Antialiasing, false);
+
+ QPen trackPen(trackColor, 2.0, Qt::CustomDashLine, Qt::FlatCap, Qt::MiterJoin);
+ trackPen.setDashPattern(QVector{1.0, 0.5});
+ painter->setPen(trackPen);
+ painter->drawLine(QPointF(trackRect.left(), trackY),
+ QPointF(trackRect.right(), trackY));
+
+ if (sliderOption->sliderPosition > sliderOption->minimum) {
+ if (darkTheme) {
+ QPen progressShadowPen(QColor(0, 40, 128, 128), 2.0,
+ Qt::CustomDashLine, Qt::FlatCap, Qt::MiterJoin);
+ progressShadowPen.setDashPattern(QVector{1.0, 0.5});
+ painter->setPen(progressShadowPen);
+ painter->drawLine(QPointF(trackRect.left(), trackY + 2.0),
+ QPointF(handleRect.center().x(), trackY + 2.0));
+ }
+
+ QPen progressPen(progressColor, 2.0, Qt::CustomDashLine,
+ Qt::FlatCap, Qt::MiterJoin);
+ progressPen.setDashPattern(QVector{1.0, 0.5});
+ painter->setPen(progressPen);
+ painter->drawLine(QPointF(trackRect.left(), trackY),
+ QPointF(handleRect.center().x(), trackY));
+ }
+
+ painter->setRenderHint(QPainter::Antialiasing, true);
+ if (darkTheme) {
+ DDrawUtils::drawShadow(painter, handleRect.toRect(), HANDLE_RADIUS, HANDLE_RADIUS,
+ QColor(31, 31, 31, 102), 6.0, QPoint(0, 4));
+
+ QLinearGradient gradient(handleRect.topLeft(), handleRect.bottomLeft());
+ gradient.setColorAt(0, QColor(83, 83, 83));
+ gradient.setColorAt(1, QColor(46, 46, 46));
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(gradient);
+ painter->drawRoundedRect(handleRect.adjusted(0.5, 0.5, -0.5, -0.5),
+ HANDLE_RADIUS, HANDLE_RADIUS);
+
+ painter->setPen(QPen(QColor(0, 0, 0, 51), 1.0));
+ painter->setBrush(Qt::NoBrush);
+ painter->drawRoundedRect(handleRect.adjusted(0.5, 0.5, -0.5, -0.5),
+ HANDLE_RADIUS, HANDLE_RADIUS);
+ painter->setPen(QPen(QColor(255, 255, 255, 26), 1.0));
+ painter->drawRoundedRect(handleRect.adjusted(1.5, 1.5, -1.5, -1.5),
+ HANDLE_RADIUS - 1.0, HANDLE_RADIUS - 1.0);
+ } else {
+ DDrawUtils::drawShadow(painter, handleRect.toRect(), HANDLE_RADIUS, HANDLE_RADIUS,
+ QColor(0, 0, 0, 77), 4.0, QPoint(0, 2));
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(QColor(77, 77, 77));
+ painter->drawRoundedRect(handleRect.adjusted(0.5, 0.5, -0.5, -0.5),
+ HANDLE_RADIUS, HANDLE_RADIUS);
+ }
+ painter->restore();
+ }
+};
+
+// 把单色图标整体着色成指定颜色(保留原始 alpha 形状)。
+// 图标资源本身固定是 #000000,DTK 对普通 QIcon 不会按 palette 重新着色,
+// 所以选中态(蓝底白图标)需要在这里显式着色。
+QIcon tintedIcon(const QIcon &icon, const QSize &iconSize, const QColor &color)
+{
+ if (icon.isNull()) {
+ return icon;
+ }
+ const qreal dpr = qApp->devicePixelRatio();
+ const QPixmap base = icon.pixmap(iconSize, dpr);
+ if (base.isNull()) {
+ return icon;
+ }
+ QPixmap tinted(base.size());
+ tinted.fill(Qt::transparent);
+ tinted.setDevicePixelRatio(dpr);
+ QPainter painter(&tinted);
+ painter.drawPixmap(0, 0, base);
+ painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
+ painter.fillRect(tinted.rect(), color);
+ painter.end();
+ return QIcon(tinted);
+}
+
+// 设计稿「截图录屏-工具栏-比例2(激活)」中的图钉按钮:
+// 36x36、圆角 8;normal 黑色 10% 背景 + 深色图标、hover 20% 背景、
+// press 更深背景 + 主题高亮色图标、active(选中)主题高亮色背景 + 白色图标。
+// 按钮仍继承 DTK DIconButton,只重写背景和图标着色,保证交互行为复用 DTK。
+class SelectionLockButton : public DIconButton
+{
+public:
+ explicit SelectionLockButton(QWidget *parent = nullptr)
+ : DIconButton(parent)
+ {
+ setCheckable(true);
+ setFlat(true);
+ }
+
+protected:
+ void initStyleOption(DStyleOptionButton *option) const override
+ {
+ DIconButton::initStyleOption(option);
+ // 选中态配色由本类自己绘制,不再交给 DTK 的蓝色选中态处理,避免与
+ // 自绘背景叠加出两层高亮。
+ option->state &= ~QStyle::State_On;
+ }
+
+ void paintEvent(QPaintEvent *event) override
+ {
+ Q_UNUSED(event);
+ const bool darkTheme = DGuiApplicationHelper::instance()->themeType()
+ == DGuiApplicationHelper::DarkType;
+ const bool active = isChecked();
+
+ // 设计稿:normal #000000 10%、hover #000000 20%,press 再深一档;
+ // 深色主题下换成同比例的白色蒙层,保证在深色底板上依然可辨。
+ QColor background;
+ if (active) {
+ background = palette().highlight().color();
+ } else if (isDown()) {
+ background = darkTheme ? QColor(255, 255, 255, 51) : QColor(0, 0, 0, 64);
+ } else if (underMouse()) {
+ background = darkTheme ? QColor(255, 255, 255, 38) : QColor(0, 0, 0, 51);
+ } else {
+ background = darkTheme ? QColor(255, 255, 255, 26) : QColor(0, 0, 0, 26);
+ }
+
+ QPainter painter(this);
+ painter.setRenderHint(QPainter::Antialiasing);
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(background);
+ painter.drawRoundedRect(rect(), 8, 8);
+ painter.end();
+
+ DStylePainter stylePainter(this);
+ DStyleOptionButton option;
+ initStyleOption(&option);
+
+ QColor iconColor;
+ if (active) {
+ iconColor = option.palette.highlightedText().color();
+ } else if (isDown()) {
+ iconColor = option.palette.highlight().color();
+ } else {
+ iconColor = darkTheme ? QColor(255, 255, 255) : QColor(0, 0, 0);
+ }
+ const QSize iconSize = option.iconSize.isValid() ? option.iconSize
+ : QSize(BUTTON_ICON_SIZE, BUTTON_ICON_SIZE);
+ option.icon = tintedIcon(option.icon, iconSize, iconColor);
+
+ stylePainter.drawControl(DStyle::CE_IconButton, option);
+ }
+};
+
+QColor blurMaskColor()
+{
+ // 与主工具栏保持一致的模糊背景:浅色 #F7F7F7 60%,深色 #000000 30%
+ if (Utils::themeType == 1) {
+ return QColor(0xF7, 0xF7, 0xF7, 153);
+ }
+ return QColor(0, 0, 0, 76);
+}
+
+// DFloatingWidget 会在内容四周预留阴影留白(常规档位为 6px),
+// 设计稿给出的 56x103 / 270x56 指的是可见底板,控件尺寸要再加上两侧留白。
+int plateShadowMargin(const QWidget *widget)
+{
+ return DStyle::pixelMetric(widget->style(), DStyle::PM_FloatingWidgetShadowMargins, nullptr, widget);
+}
+
+QWidget *createFloatingPlate(QWidget *parent, const QSize &plateSize, int radius, int margin)
+{
+ // 与主工具栏(ToolBarWidget/DFloatingWidget)保持同一套悬浮样式:
+ // 高斯模糊背景 + DTK 悬浮阴影(设计稿:偏移 0/4、模糊 8、10% 黑)
+ DFloatingWidget *plate = new DFloatingWidget(parent);
+ plate->setBlurBackgroundEnabled(true);
+ plate->blurBackground()->setRadius(30);
+ plate->blurBackground()->setMode(DBlurEffectWidget::GaussianBlur);
+ plate->blurBackground()->setBlurEnabled(true);
+ plate->blurBackground()->setBlendMode(DBlurEffectWidget::InWidgetBlend);
+ plate->blurBackground()->setMaskColor(blurMaskColor());
+ plate->setFramRadius(radius);
+ plate->setFixedSize(plateSize.width() + margin * 2, plateSize.height() + margin * 2);
+ return plate;
+}
+
+// 设计稿 DTK/输入框/Spinbox2:输入框底色为 #FFFFFF 60%。
+// DTK/chameleon 的 embed 样式是按 QPalette::Button 取这个底色的,
+// 默认浅色主题下该角色与悬浮面板底色几乎一致(实测 228 vs 229),
+// 结果设计稿里那个圆角输入框在界面上"消失"了。这里按设计稿显式指定底色。
+void applySpinBoxPlateColor(DSpinBox *spin)
+{
+ QPalette pal = spin->palette();
+ pal.setColor(QPalette::Button, QColor(255, 255, 255, 153));
+ spin->setPalette(pal);
+}
+
+void applyLineEditPlateColor(DLineEdit *edit)
+{
+ if (!edit) {
+ return;
+ }
+
+ const QColor plateColor(255, 255, 255, 153);
+ QPalette pal = edit->palette();
+ pal.setColor(QPalette::Base, plateColor);
+ pal.setColor(QPalette::Button, plateColor);
+ edit->setPalette(pal);
+
+ if (QLineEdit *lineEdit = edit->lineEdit()) {
+ QPalette lineEditPal = lineEdit->palette();
+ lineEditPal.setColor(QPalette::Base, plateColor);
+ lineEditPal.setColor(QPalette::Button, plateColor);
+ lineEdit->setPalette(lineEditPal);
+ }
+}
+
+int greatestCommonDivisor(int a, int b)
+{
+ a = qAbs(a);
+ b = qAbs(b);
+ while (b != 0) {
+ const int remainder = a % b;
+ a = b;
+ b = remainder;
+ }
+ return qMax(1, a);
+}
+
+QPair simplifiedRatio(int width, int height)
+{
+ const int divisor = greatestCommonDivisor(width, height);
+ return {qMax(1, width / divisor), qMax(1, height / divisor)};
+}
+
+DLabel *createIconLabel(QWidget *parent, const QString &iconName, const QString &objectName,
+ const QSize &size, const QSize &iconSize)
+{
+ DLabel *label = new DLabel(parent);
+ label->setObjectName(objectName);
+ label->setAccessibleName(objectName);
+ label->setFixedSize(size);
+ label->setAlignment(Qt::AlignCenter);
+ label->setPixmap(QIcon::fromTheme(iconName).pixmap(iconSize));
+ return label;
+}
+
+}
+
+RoundedCornerWidget::RoundedCornerWidget(QWidget *parent)
+ : QWidget(parent)
+{
+ setAttribute(Qt::WA_TranslucentBackground);
+ setAttribute(Qt::WA_NoSystemBackground);
+ const int margin = plateShadowMargin(this);
+ setFixedSize(GROUP_WIDTH + margin * 2, GROUP_HEIGHT + margin * 2);
+ initUi();
+ syncFromConfig();
+}
+
+RoundedCornerWidget::~RoundedCornerWidget()
+{
+ flushPendingPersist();
+}
+
+void RoundedCornerWidget::initUi()
+{
+ m_groupFrame = buildGroup();
+ m_cornerPanel = buildCornerPanel();
+ m_sizePanel = buildSizePanel();
+
+ m_cornerPanel->hide();
+ m_sizePanel->hide();
+
+ // 圆角按钮:单击只展开/收起属性面板并高亮按钮。
+ // 圆角半径默认保持 0(设计稿:0px~60px,默认 0),需要用户拖动滑块或输入数值才真正生效。
+ connect(m_cornerButton, &ToolButton::clicked, this, [this]() {
+ setPanel(m_panel == CornerPanel ? NoPanel : CornerPanel);
+ syncButtonStates();
+ });
+
+ // 尺寸/比例按钮:展开/收起尺寸比例面板
+ connect(m_ratioButton, &ToolButton::clicked, this, [this]() {
+ setPanel(m_panel == SizePanel ? NoPanel : SizePanel);
+ syncButtonStates();
+ });
+
+ // 拖动滑块时每一步都写配置会造成明显卡顿:QSettings::sync() 在 /home 上单次实测
+ // 可达 150ms 以上。这里改为防抖落盘(松手/输入完成/控件隐藏时立即写入)。
+ m_persistTimer = new QTimer(this);
+ m_persistTimer->setSingleShot(true);
+ m_persistTimer->setInterval(300);
+ connect(m_persistTimer, &QTimer::timeout, this, &RoundedCornerWidget::persistRadius);
+
+ connect(m_radiusSlider, &DSlider::valueChanged, this, [this](int value) {
+ if (m_radiusSpin->value() != value) {
+ m_radiusSpin->setValue(value);
+ }
+ if (m_radius == value) {
+ return;
+ }
+ m_radius = value;
+ m_persistTimer->start();
+ emit radiusChanged(m_radius);
+ });
+ // 松开滑块 / 输入框结束编辑:立刻落盘,保证拖完就记住
+ connect(m_radiusSlider, &DSlider::sliderReleased, this, [this]() {
+ flushPendingPersist();
+ });
+ connect(m_radiusSpin, QOverload::of(&DSpinBox::valueChanged), this, [this](int value) {
+ if (m_radiusSlider->value() != value) {
+ m_radiusSlider->setValue(value);
+ }
+ });
+ connect(m_radiusSpin, &DSpinBox::editingFinished, this, [this]() {
+ flushPendingPersist();
+ });
+
+ // 面板输入框里回车代表“确认当前数值”:确认后主动移出焦点,
+ // 这样紧接着再按一次回车才会轮到主窗口的“保存截图”快捷方式。
+ const QList panelEditors = {
+ m_radiusSpin->lineEdit(),
+ m_widthEdit->lineEdit(),
+ m_heightEdit->lineEdit(),
+ };
+ for (QLineEdit *editor : panelEditors) {
+ if (!editor) {
+ continue;
+ }
+ connect(editor, &QLineEdit::returnPressed, this, [editor]() {
+ editor->clearFocus();
+ });
+ }
+
+ // 宽/高输入框用 DLineEdit,整段编辑完成(回车或失焦)后再提交,避免输入中途反复改选区。
+ connect(m_widthEdit, &DLineEdit::editingFinished, this, [this]() {
+ if (m_syncingSelectionSize) {
+ return;
+ }
+
+ const int maxValue = m_ratioLocked ? MAX_RATIO_VALUE : MAX_DIMENSION_VALUE;
+ const int widthValue = qBound(1, m_widthEdit->text().toInt(), maxValue);
+ const int heightValue = qBound(1, m_heightEdit->text().toInt(), maxValue);
+ if (m_ratioLocked) {
+ const int anchorHeight = qMax(1, m_selectionHeight);
+ m_selectionWidth = qMax(1, qRound(anchorHeight * static_cast(widthValue) / heightValue));
+ m_selectionHeight = anchorHeight;
+ } else {
+ m_selectionWidth = widthValue;
+ m_selectionHeight = heightValue;
+ }
+ updateDimensionFields();
+ persistFixedSize();
+ emit selectionSizeChanged(m_selectionWidth, m_selectionHeight);
+ });
+ connect(m_heightEdit, &DLineEdit::editingFinished, this, [this]() {
+ if (m_syncingSelectionSize) {
+ return;
+ }
+
+ const int maxValue = m_ratioLocked ? MAX_RATIO_VALUE : MAX_DIMENSION_VALUE;
+ const int widthValue = qBound(1, m_widthEdit->text().toInt(), maxValue);
+ const int heightValue = qBound(1, m_heightEdit->text().toInt(), maxValue);
+ if (m_ratioLocked) {
+ const int anchorWidth = qMax(1, m_selectionWidth);
+ m_selectionHeight = qMax(1, qRound(anchorWidth * static_cast(heightValue) / widthValue));
+ m_selectionWidth = anchorWidth;
+ } else {
+ m_selectionWidth = widthValue;
+ m_selectionHeight = heightValue;
+ }
+ updateDimensionFields();
+ persistFixedSize();
+ emit selectionSizeChanged(m_selectionWidth, m_selectionHeight);
+ });
+
+ connect(m_lockButton, &DIconButton::clicked, this, [this]() {
+ m_rememberSetting = m_lockButton->isChecked();
+ if (m_rememberSetting) {
+ // 图钉只负责“下次启动继续使用”,当前这次框选仍由用户自己控制。
+ // 勾选时把面板当前值固化为记忆值,避免只有图钉状态而没有参数。
+ captureCurrentSetting();
+ }
+ m_pendingRememberedSetting = false;
+ persistRememberSetting();
+ persistFixedSize();
+ syncButtonStates();
+ });
+}
+
+QWidget *RoundedCornerWidget::buildGroup()
+{
+ const int margin = plateShadowMargin(this);
+ QWidget *group = createFloatingPlate(this, QSize(GROUP_WIDTH, GROUP_HEIGHT), PANEL_RADIUS, margin);
+
+ m_cornerButton = new ToolButton(group);
+ m_cornerButton->setFixedSize(BUTTON_SIZE, BUTTON_SIZE);
+ m_cornerButton->setIconSize(QSize(BUTTON_ICON_SIZE, BUTTON_ICON_SIZE));
+ m_cornerButton->setCheckable(true);
+ m_cornerButton->setIcon(QIcon::fromTheme("corner-radius"));
+ m_cornerButton->setObjectName("RoundedCornerButton");
+ m_cornerButton->setAccessibleName("RoundedCornerButton");
+ m_cornerButton->setToolTip(tr("Radius"));
+ m_cornerButton->move(margin + BUTTON_MARGIN_X, margin + FIRST_BUTTON_Y);
+
+ m_ratioButton = new ToolButton(group);
+ m_ratioButton->setFixedSize(BUTTON_SIZE, BUTTON_SIZE);
+ m_ratioButton->setIconSize(QSize(BUTTON_ICON_SIZE, BUTTON_ICON_SIZE));
+ m_ratioButton->setCheckable(true);
+ m_ratioButton->setIcon(QIcon::fromTheme("size-ratio"));
+ m_ratioButton->setObjectName("SizeRatioButton");
+ m_ratioButton->setAccessibleName("SizeRatioButton");
+ m_ratioButton->setToolTip(tr("Size / ratio"));
+ m_ratioButton->move(margin + BUTTON_MARGIN_X, margin + SECOND_BUTTON_Y);
+
+ return group;
+}
+
+QWidget *RoundedCornerWidget::buildCornerPanel()
+{
+ const int margin = plateShadowMargin(this);
+ QWidget *panel = createFloatingPlate(this, QSize(PANEL_WIDTH, PANEL_HEIGHT), PANEL_RADIUS, margin);
+
+ DLabel *leading = createIconLabel(panel, "corner-adjust", "CornerAdjustIcon",
+ QSize(ADJUST_ICON_SIZE, ADJUST_ICON_SIZE),
+ QSize(ADJUST_ICON_SIZE, ADJUST_ICON_SIZE));
+ leading->move(margin + PANEL_PADDING, margin + (PANEL_HEIGHT - ADJUST_ICON_SIZE) / 2);
+
+ m_radiusSlider = new DSlider(Qt::Horizontal, panel);
+ m_radiusSlider->setMinimum(0);
+ m_radiusSlider->setMaximum(RADIUS_MAX);
+ m_radiusSlider->setFixedSize(SLIDER_WIDTH, SLIDER_HEIGHT);
+ m_radiusSlider->setObjectName("CornerRadiusSlider");
+ m_radiusSlider->setAccessibleName("CornerRadiusSlider");
+ m_radiusSlider->move(margin + SLIDER_X, margin + SLIDER_Y);
+ QSlider *radiusSlider = m_radiusSlider->slider();
+ radiusSlider->setStyle(new RoundedSliderStyle(radiusSlider));
+ connect(DGuiApplicationHelper::instance(), &DGuiApplicationHelper::themeTypeChanged,
+ radiusSlider, qOverload<>(&QWidget::update));
+
+ m_radiusSpin = new DSpinBox(panel);
+ m_radiusSpin->setEnabledEmbedStyle(true);
+ applySpinBoxPlateColor(m_radiusSpin);
+ m_radiusSpin->setRange(0, RADIUS_MAX);
+ m_radiusSpin->setFixedSize(SPIN_WIDTH, SPIN_HEIGHT);
+ m_radiusSpin->setObjectName("CornerRadiusSpinBox");
+ m_radiusSpin->setAccessibleName("CornerRadiusSpinBox");
+ m_radiusSpin->move(margin + PANEL_WIDTH - PANEL_PADDING - SPIN_WIDTH,
+ margin + (PANEL_HEIGHT - SPIN_HEIGHT) / 2);
+
+ return panel;
+}
+
+QWidget *RoundedCornerWidget::buildSizePanel()
+{
+ const int margin = plateShadowMargin(this);
+ QWidget *panel = createFloatingPlate(this, QSize(PANEL_WIDTH, PANEL_HEIGHT), PANEL_RADIUS, margin);
+
+ // 设计稿「尺寸 ▾」:55x24,前面是文字,后面是下拉箭头。
+ // 用 DTK 的 DPushButton(QPushButton 派生)挂 DMenu,按钮自带“文本 + 下拉箭头”,
+ // 不去自绘箭头,也不依赖 ComboBox 的额外内边距。
+ m_sizeModeButton = new DPushButton(panel);
+ m_sizeModeButton->setFlat(true);
+ // Sketch 里整块下拉区可见宽度是 55;DPushButton 自带内边距,
+ // 固定 55 会把“尺寸”文字裁掉,这里放宽到贴住右侧输入框的 64。
+ m_sizeModeButton->setFixedSize(64, SIZE_MODE_HEIGHT);
+ m_sizeModeButton->setObjectName("SizeRatioModeButton");
+ m_sizeModeButton->setAccessibleName("SizeRatioModeButton");
+ m_sizeModeButton->setToolTip(tr("Size / ratio"));
+ m_sizeModeButton->setText(tr("Size"));
+ DFontSizeManager::instance()->bind(m_sizeModeButton, DFontSizeManager::T6);
+
+ DMenu *modeMenu = new DMenu(m_sizeModeButton);
+ QAction *sizeAction = modeMenu->addAction(tr("Size"));
+ QAction *ratioAction = modeMenu->addAction(tr("Ratio"));
+ sizeAction->setCheckable(true);
+ ratioAction->setCheckable(true);
+ sizeAction->setChecked(true);
+ QActionGroup *modeGroup = new QActionGroup(modeMenu);
+ modeGroup->setExclusive(true);
+ modeGroup->addAction(sizeAction);
+ modeGroup->addAction(ratioAction);
+ connect(modeGroup, &QActionGroup::triggered, this, [this, ratioAction](QAction *action) {
+ if (!action || !m_sizeModeButton) {
+ return;
+ }
+ const bool locked = (action == ratioAction);
+ m_sizeModeButton->setText(action->text());
+ if (m_ratioLocked != locked) {
+ m_ratioLocked = locked;
+ persistRatioType();
+ // 图钉已经选中时,前面切换“尺寸/比例”也属于修改记忆设置,
+ // 立即同步 remember_ratio,保证图钉、下拉选项和实际参数一致。
+ if (m_rememberSetting) {
+ captureCurrentSetting();
+ }
+ }
+ updateDimensionFields();
+ syncButtonStates();
+ });
+ m_sizeModeButton->setMenu(modeMenu);
+ m_sizeModeButton->move(margin + SIZE_MODE_X, margin + SIZE_MODE_Y);
+
+ // 宽/高输入框:设计稿 60x36 的 DTK 输入框,无上下调节按钮,数字居中。
+ m_widthEdit = new DLineEdit(panel);
+ applyLineEditPlateColor(m_widthEdit);
+ m_widthEdit->setFixedSize(DIMENSION_SPIN_WIDTH, SPIN_HEIGHT);
+ m_widthEdit->setClearButtonEnabled(false);
+ m_widthEdit->setObjectName("SelectionWidthEdit");
+ m_widthEdit->setAccessibleName("SelectionWidthEdit");
+ m_widthEdit->lineEdit()->setAlignment(Qt::AlignCenter);
+ m_widthValidator = new QIntValidator(1, MAX_DIMENSION_VALUE, m_widthEdit);
+ m_widthEdit->lineEdit()->setValidator(m_widthValidator);
+ DFontSizeManager::instance()->bind(m_widthEdit, DFontSizeManager::T6);
+ m_widthEdit->move(margin + WIDTH_SPIN_X, margin + (PANEL_HEIGHT - SPIN_HEIGHT) / 2);
+
+ DLabel *separator = new DLabel(QString(QChar(0x00D7)), panel);
+ separator->setFixedSize(DIMENSION_SEPARATOR_SIZE, DIMENSION_SEPARATOR_SIZE);
+ separator->setAlignment(Qt::AlignCenter);
+ separator->setObjectName("SelectionSizeSeparator");
+ separator->setAccessibleName("SelectionSizeSeparator");
+ DFontSizeManager::instance()->bind(separator, DFontSizeManager::T6);
+ separator->move(margin + DIMENSION_SEPARATOR_X, margin + DIMENSION_SEPARATOR_Y);
+
+ m_heightEdit = new DLineEdit(panel);
+ applyLineEditPlateColor(m_heightEdit);
+ m_heightEdit->setFixedSize(DIMENSION_SPIN_WIDTH, SPIN_HEIGHT);
+ m_heightEdit->setClearButtonEnabled(false);
+ m_heightEdit->setObjectName("SelectionHeightEdit");
+ m_heightEdit->setAccessibleName("SelectionHeightEdit");
+ m_heightEdit->lineEdit()->setAlignment(Qt::AlignCenter);
+ m_heightValidator = new QIntValidator(1, MAX_DIMENSION_VALUE, m_heightEdit);
+ m_heightEdit->lineEdit()->setValidator(m_heightValidator);
+ DFontSizeManager::instance()->bind(m_heightEdit, DFontSizeManager::T6);
+ m_heightEdit->move(margin + HEIGHT_SPIN_X, margin + (PANEL_HEIGHT - SPIN_HEIGHT) / 2);
+
+ m_lockButton = new SelectionLockButton(panel);
+ m_lockButton->setFixedSize(BUTTON_SIZE, BUTTON_SIZE);
+ m_lockButton->setIconSize(QSize(BUTTON_ICON_SIZE, BUTTON_ICON_SIZE));
+ m_lockButton->setIcon(QIcon::fromTheme("selection-fixed"));
+ m_lockButton->setObjectName("RatioLockButton");
+ m_lockButton->setAccessibleName("RatioLockButton");
+ m_lockButton->setToolTip(tr("Keep the selected size or ratio for the next screenshot"));
+ DStyle::setFocusRectVisible(m_lockButton, false);
+ m_lockButton->move(margin + LOCK_BUTTON_X, margin + (PANEL_HEIGHT - BUTTON_SIZE) / 2);
+
+ return panel;
+}
+
+void RoundedCornerWidget::syncFromConfig()
+{
+ ConfigSettings *config = ConfigSettings::instance();
+ m_radius = config->getValue("rounded", "radius").toInt();
+ m_radius = qBound(0, m_radius, RADIUS_MAX);
+ m_persistedRadius = m_radius;
+ m_radiusSlider->setValue(m_radius);
+ m_radiusSpin->setValue(m_radius);
+
+ // ratio_type 只表示面板当前下拉选择:0 尺寸、1 比例;图钉单独用 remember 保存。
+ // 此前两者共用 ratio_type,导致仅切换“比例”也会在下次启动被当成已勾选图钉。
+ const bool dropdownRatio = config->getValue("rounded", "ratio_type").toInt() > 0;
+ m_ratioLocked = dropdownRatio;
+ m_rememberSetting = config->getValue("rounded", "remember").toBool();
+ m_rememberedIsRatio = config->getValue("rounded", "remember_ratio").toBool();
+ m_fixedWidth = qMax(0, config->getValue("rounded", "fixed_width").toInt());
+ m_fixedHeight = qMax(0, config->getValue("rounded", "fixed_height").toInt());
+
+ // 图钉选中时,以 remember_ratio 作为权威模式回显:这样下次打开时
+ // 图钉的 checked 状态、下拉菜单的 checked 项和实际要应用的参数三者一致。
+ if (m_rememberSetting) {
+ m_ratioLocked = m_rememberedIsRatio;
+ if (dropdownRatio != m_ratioLocked) {
+ persistRatioType();
+ }
+ }
+ m_pendingRememberedSetting = m_rememberSetting && m_fixedWidth > 0 && m_fixedHeight > 0;
+
+ m_lockButton->setChecked(m_rememberSetting);
+ if (m_sizeModeButton && m_sizeModeButton->menu()) {
+ QList actions = m_sizeModeButton->menu()->actions();
+ if (actions.size() > 1) {
+ actions.at(m_ratioLocked ? 1 : 0)->setChecked(true);
+ m_sizeModeButton->setText(actions.at(m_ratioLocked ? 1 : 0)->text());
+ }
+ }
+ syncButtonStates();
+}
+
+void RoundedCornerWidget::syncButtonStates()
+{
+ // 圆角按钮高亮表示面板已展开或圆角已开启;尺寸比例按钮高亮表示面板展开或已锁定比例
+ m_cornerButton->setChecked(m_panel == CornerPanel || m_radius > 0);
+ m_ratioButton->setChecked(m_panel == SizePanel || m_ratioLocked);
+}
+
+void RoundedCornerWidget::persistRadius()
+{
+ if (m_persistedRadius == m_radius) {
+ return;
+ }
+ m_persistedRadius = m_radius;
+ ConfigSettings::instance()->setValue("rounded", "radius", m_radius);
+}
+
+// 把防抖待写入的值立刻落盘(松手、输入完成、控件隐藏/销毁时调用)
+void RoundedCornerWidget::flushPendingPersist()
+{
+ if (m_persistTimer && m_persistTimer->isActive()) {
+ m_persistTimer->stop();
+ }
+ persistRadius();
+}
+
+void RoundedCornerWidget::hideEvent(QHideEvent *event)
+{
+ flushPendingPersist();
+ QWidget::hideEvent(event);
+}
+
+void RoundedCornerWidget::persistRatioType()
+{
+ ConfigSettings::instance()->setValue("rounded", "ratio_type", m_ratioLocked ? 1 : 0);
+}
+
+void RoundedCornerWidget::persistRememberSetting()
+{
+ ConfigSettings::instance()->setValue("rounded", "remember", m_rememberSetting);
+}
+
+void RoundedCornerWidget::persistFixedSize()
+{
+ if (m_selectionWidth <= 0 || m_selectionHeight <= 0) {
+ return;
+ }
+
+ m_rememberedIsRatio = m_ratioLocked;
+ if (m_rememberedIsRatio) {
+ const QPair ratio = simplifiedRatio(m_selectionWidth, m_selectionHeight);
+ m_fixedWidth = ratio.first;
+ m_fixedHeight = ratio.second;
+ } else {
+ m_fixedWidth = qMax(1, m_selectionWidth);
+ m_fixedHeight = qMax(1, m_selectionHeight);
+ }
+
+ ConfigSettings *config = ConfigSettings::instance();
+ config->setValue("rounded", "remember_ratio", m_rememberedIsRatio);
+ config->setValue("rounded", "fixed_width", m_fixedWidth);
+ config->setValue("rounded", "fixed_height", m_fixedHeight);
+}
+
+void RoundedCornerWidget::captureCurrentSetting()
+{
+ persistFixedSize();
+}
+
+bool RoundedCornerWidget::hasPendingRememberedSetting() const
+{
+ return m_pendingRememberedSetting && m_rememberSetting
+ && m_fixedWidth > 0 && m_fixedHeight > 0;
+}
+
+bool RoundedCornerWidget::isRememberedSettingRatio() const
+{
+ return m_rememberedIsRatio;
+}
+
+QSize RoundedCornerWidget::rememberedSetting() const
+{
+ return QSize(m_fixedWidth, m_fixedHeight);
+}
+
+void RoundedCornerWidget::consumeRememberedSetting()
+{
+ m_pendingRememberedSetting = false;
+}
+
+int RoundedCornerWidget::radius() const
+{
+ return m_radius;
+}
+
+bool RoundedCornerWidget::isCornerEnabled() const
+{
+ return m_radius > 0;
+}
+
+bool RoundedCornerWidget::isRatioLocked() const
+{
+ return m_ratioLocked;
+}
+
+void RoundedCornerWidget::setRadius(int radius)
+{
+ m_radiusSlider->setValue(qBound(0, radius, RADIUS_MAX));
+}
+
+void RoundedCornerWidget::setPanel(PanelType panel)
+{
+ if (m_panel == panel) {
+ return;
+ }
+ m_panel = panel;
+ updateLayout();
+ reposition();
+ emit panelExpandedChanged(m_panel != NoPanel);
+}
+
+void RoundedCornerWidget::collapsePanels()
+{
+ flushPendingPersist();
+ if (m_panel != NoPanel) {
+ setPanel(NoPanel);
+ }
+}
+
+void RoundedCornerWidget::setSelectionSize(int width, int height)
+{
+ m_selectionWidth = width;
+ m_selectionHeight = height;
+ updateDimensionFields();
+}
+
+void RoundedCornerWidget::updateDimensionFields()
+{
+ if (!m_widthEdit || !m_heightEdit) {
+ return;
+ }
+
+ const int maxValue = m_ratioLocked ? MAX_RATIO_VALUE : MAX_DIMENSION_VALUE;
+ if (m_widthValidator) {
+ m_widthValidator->setRange(1, maxValue);
+ }
+ if (m_heightValidator) {
+ m_heightValidator->setRange(1, maxValue);
+ }
+
+ m_syncingSelectionSize = true;
+ if (m_ratioLocked && m_selectionWidth > 0 && m_selectionHeight > 0) {
+ const QPair ratio = simplifiedRatio(m_selectionWidth, m_selectionHeight);
+ if (m_widthEdit->text().toInt() != ratio.first) {
+ m_widthEdit->setText(QString::number(ratio.first));
+ }
+ if (m_heightEdit->text().toInt() != ratio.second) {
+ m_heightEdit->setText(QString::number(ratio.second));
+ }
+ } else {
+ if (m_widthEdit->text().toInt() != m_selectionWidth) {
+ m_widthEdit->setText(QString::number(m_selectionWidth));
+ }
+ if (m_heightEdit->text().toInt() != m_selectionHeight) {
+ m_heightEdit->setText(QString::number(m_selectionHeight));
+ }
+ }
+ m_syncingSelectionSize = false;
+}
+
+void RoundedCornerWidget::updateLayout()
+{
+ const bool hasPanel = (m_panel != NoPanel);
+ const int panelHeightOffset = (m_panel == SizePanel) ? SECOND_BUTTON_Y + BUTTON_SIZE / 2 : FIRST_BUTTON_Y + BUTTON_SIZE / 2;
+ const int totalWidth = hasPanel ? (GROUP_WIDTH + PANEL_GAP + PANEL_WIDTH) : GROUP_WIDTH;
+ const int margin = plateShadowMargin(this);
+
+ // 控件尺寸 = 可见底板尺寸 + DFloatingWidget 的阴影留白
+ setFixedSize(totalWidth + margin * 2, GROUP_HEIGHT + margin * 2);
+
+ const int groupX = (hasPanel && m_panelOnLeft) ? (PANEL_WIDTH + PANEL_GAP) : 0;
+ m_groupFrame->move(groupX, 0);
+
+ const int panelX = m_panelOnLeft ? 0 : (GROUP_WIDTH + PANEL_GAP);
+ const int panelY = panelHeightOffset - PANEL_HEIGHT / 2;
+
+ m_cornerPanel->setVisible(m_panel == CornerPanel);
+ m_sizePanel->setVisible(m_panel == SizePanel);
+ m_cornerPanel->move(panelX, panelY);
+ m_sizePanel->move(panelX, panelY);
+
+ if (m_panel != NoPanel) {
+ (m_panel == CornerPanel ? m_cornerPanel : m_sizePanel)->raise();
+ }
+}
+
+void RoundedCornerWidget::followSelection(const QRect &selection, const QRect &screen)
+{
+ if (selection.isEmpty() || screen.isEmpty()) {
+ return;
+ }
+ m_lastSelection = selection;
+ m_lastScreen = screen;
+ reposition();
+}
+
+void RoundedCornerWidget::reposition()
+{
+ const QRect &selection = m_lastSelection;
+ const QRect &screen = m_lastScreen;
+ if (selection.isEmpty() || screen.isEmpty()) {
+ return;
+ }
+
+ // 面板展开方向与位置都以「按钮组 + 面板」的完整宽度计算,
+ // 保证面板展开前后按钮组位置不会跳动(设计稿 ①/②/③ 判定条件)。
+ const int totalWidth = GROUP_WIDTH + PANEL_GAP + PANEL_WIDTH;
+ const bool hasPanel = (m_panel != NoPanel);
+
+ // 按钮组在屏幕坐标系下应贴合的左边位置
+ int groupLeft = 0;
+ bool panelOnLeft = false;
+ // ① 选区右侧空间足够:按钮组在选区右上角右侧,面板向右展开
+ if (selection.right() + SELECTION_GAP + totalWidth <= screen.right()) {
+ groupLeft = selection.right() + SELECTION_GAP;
+ } else if (selection.left() - SELECTION_GAP - totalWidth >= screen.left()) {
+ // ② 选区左侧空间足够:按钮组在选区左上角左侧,面板向左展开
+ panelOnLeft = true;
+ groupLeft = selection.left() - SELECTION_GAP - GROUP_WIDTH;
+ } else {
+ // ③ 两侧均不足:按钮组贴在选区右上角内侧,面板向左展开
+ panelOnLeft = true;
+ groupLeft = selection.right() - GROUP_WIDTH + 1;
+ }
+
+ if (m_panelOnLeft != panelOnLeft) {
+ m_panelOnLeft = panelOnLeft;
+ updateLayout();
+ }
+
+ // 面板向左展开时按钮组在控件内部有固定偏移;面板收起时该偏移为 0。
+ // 必须按当前展开状态换算控件原点,否则收起面板时按钮组会偏移一个面板宽度。
+ const int groupOffset = (panelOnLeft && hasPanel) ? (PANEL_WIDTH + PANEL_GAP) : 0;
+ // 控件左上角比可见底板多出一个阴影留白,定位时要去掉
+ const int margin = plateShadowMargin(this);
+ int x = groupLeft - groupOffset - margin;
+ int y = selection.top() - margin;
+
+ x = qBound(screen.left(), x, qMax(screen.left(), screen.right() - width() + 1));
+ y = qBound(screen.top(), y, qMax(screen.top(), screen.bottom() - height() + 1));
+ move(x, y);
+}
+
+QPixmap RoundedCornerWidget::applyRoundedCorners(const QPixmap &source, int radius)
+{
+ if (source.isNull() || radius <= 0) {
+ return source;
+ }
+ QImage image = source.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
+ const qreal dpr = image.devicePixelRatio() > 0 ? image.devicePixelRatio() : 1.0;
+ const qreal scaledRadius = radius * dpr;
+ QImage out(image.size(), QImage::Format_ARGB32_Premultiplied);
+ out.fill(Qt::transparent);
+ out.setDevicePixelRatio(dpr);
+
+ QPainter painter(&out);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
+ QPainterPath path;
+ path.addRoundedRect(QRectF(0, 0, image.width(), image.height()), scaledRadius, scaledRadius);
+ painter.setClipPath(path);
+ painter.drawImage(0, 0, image);
+ painter.end();
+
+ QPixmap result = QPixmap::fromImage(out);
+ result.setDevicePixelRatio(dpr);
+ return result;
+}
+
+QPixmap RoundedCornerWidget::compositeOnWhite(const QPixmap &source)
+{
+ if (source.isNull() || !source.hasAlphaChannel()) {
+ return source;
+ }
+ QImage image = source.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
+ const qreal dpr = image.devicePixelRatio() > 0 ? image.devicePixelRatio() : 1.0;
+ QImage out(image.size(), QImage::Format_ARGB32_Premultiplied);
+ out.fill(Qt::white);
+ out.setDevicePixelRatio(dpr);
+
+ QPainter painter(&out);
+ painter.drawImage(0, 0, image);
+ painter.end();
+
+ QPixmap result = QPixmap::fromImage(out);
+ result.setDevicePixelRatio(dpr);
+ return result;
+}
diff --git a/src/widgets/roundedcornerwidget.h b/src/widgets/roundedcornerwidget.h
new file mode 100644
index 000000000..bd6ba20fe
--- /dev/null
+++ b/src/widgets/roundedcornerwidget.h
@@ -0,0 +1,142 @@
+// Copyright (C) 2020 ~ 2021 Uniontech Software Technology Co.,Ltd.
+// SPDX-FileCopyrightText: 2022-2026 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ROUNDEDCORNERWIDGET_H
+#define ROUNDEDCORNERWIDGET_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+DWIDGET_USE_NAMESPACE
+
+class ToolButton;
+
+/**
+ * @brief 选区右上角的圆角 / 尺寸比例工具组。
+ *
+ * 对齐设计稿「截图录屏-工具栏-圆角」「截图录屏-工具栏-比例」:
+ * 56x103 竖排按钮组(圆角按钮在上、尺寸/比例按钮在下),
+ * 属性面板 270x56 在按钮组左侧或右侧水平展开。
+ *
+ * 本控件只是按钮组与面板的容器,容器本身保持透明,
+ * 按钮组与面板背景使用 DTK 的 DBlurEffectWidget(与主工具栏一致的模糊背景)。
+ */
+class RoundedCornerWidget : public QWidget
+{
+ Q_OBJECT
+public:
+ enum PanelType {
+ NoPanel = 0,
+ CornerPanel,
+ SizePanel
+ };
+
+ explicit RoundedCornerWidget(QWidget *parent = nullptr);
+ ~RoundedCornerWidget() override;
+
+ int radius() const;
+ bool isCornerEnabled() const;
+ void setRadius(int radius);
+
+ bool isRatioLocked() const;
+ PanelType currentPanel() const { return m_panel; }
+
+ // 本次启动是否还有待应用的记忆尺寸/比例。图钉开启且配置中有有效值时,
+ // 第一次完成框选后由主窗口应用一次。
+ bool hasPendingRememberedSetting() const;
+ bool isRememberedSettingRatio() const;
+ QSize rememberedSetting() const;
+ void consumeRememberedSetting();
+
+ /**
+ * @brief 跟随选区右上角;按设计稿优先级计算位置与面板展开方向
+ * ① 选区右侧空间足够 -> 按钮组在选区右上角右侧,面板向右展开
+ * ② 右侧不足、左侧足够 -> 按钮组在选区左上角左侧,面板向左展开
+ * ③ 两侧均不足 -> 按钮组在选区右上角内侧,面板向左展开
+ */
+ void followSelection(const QRect &selection, const QRect &screen);
+ // 主窗口同步当前选区尺寸到尺寸/比例面板
+ void setSelectionSize(int width, int height);
+
+ // 生成带圆角透明通道的图片;radius <= 0 时返回原图
+ static QPixmap applyRoundedCorners(const QPixmap &source, int radius);
+ // JPG/BMP 等不支持透明格式:把含 Alpha 的成图合成到白底上,
+ // 使圆角外区域呈现白色(投影等半透明像素也一并正确压在白色上)。
+ static QPixmap compositeOnWhite(const QPixmap &source);
+
+signals:
+ void radiusChanged(int radius);
+ // 用户在尺寸/比例面板中输入了新的选区尺寸
+ void selectionSizeChanged(int width, int height);
+ void panelExpandedChanged(bool expanded);
+
+public slots:
+ // 点击其它工具时收起属性面板(保留圆角与尺寸比例设置)
+ void collapsePanels();
+
+private:
+ void initUi();
+ QWidget *buildGroup();
+ QWidget *buildCornerPanel();
+ QWidget *buildSizePanel();
+ void updateLayout();
+ void reposition();
+ void setPanel(PanelType panel);
+ void syncFromConfig();
+ void persistRadius();
+ void flushPendingPersist();
+ void hideEvent(QHideEvent *event) override;
+ void persistRatioType();
+ void persistRememberSetting();
+ void persistFixedSize();
+ void captureCurrentSetting();
+ void syncButtonStates();
+ void updateDimensionFields();
+
+ QWidget *m_groupFrame = nullptr;
+ ToolButton *m_cornerButton = nullptr;
+ ToolButton *m_ratioButton = nullptr;
+ QWidget *m_cornerPanel = nullptr;
+ QWidget *m_sizePanel = nullptr;
+ DSlider *m_radiusSlider = nullptr;
+ DSpinBox *m_radiusSpin = nullptr;
+ DPushButton *m_sizeModeButton = nullptr;
+ DLineEdit *m_widthEdit = nullptr;
+ DLineEdit *m_heightEdit = nullptr;
+ QIntValidator *m_widthValidator = nullptr;
+ QIntValidator *m_heightValidator = nullptr;
+ DIconButton *m_lockButton = nullptr;
+
+ QTimer *m_persistTimer = nullptr;
+ int m_radius = 0;
+ // 已经写入配置文件的值,避免无变化的重复落盘
+ int m_persistedRadius = -1;
+ bool m_ratioLocked = false;
+ bool m_rememberSetting = false;
+ bool m_pendingRememberedSetting = false;
+ bool m_rememberedIsRatio = false;
+ int m_fixedWidth = 0;
+ int m_fixedHeight = 0;
+ int m_selectionWidth = 0;
+ int m_selectionHeight = 0;
+ PanelType m_panel = NoPanel;
+ bool m_panelOnLeft = false;
+ QRect m_lastSelection;
+ QRect m_lastScreen;
+ bool m_syncingSelectionSize = false;
+};
+
+#endif // ROUNDEDCORNERWIDGET_H
diff --git a/translations/deepin-screen-recorder_zh_CN.ts b/translations/deepin-screen-recorder_zh_CN.ts
index 60703b94c..77f676044 100644
--- a/translations/deepin-screen-recorder_zh_CN.ts
+++ b/translations/deepin-screen-recorder_zh_CN.ts
@@ -854,4 +854,31 @@ Hold down Shift to draw squares or circles.
退出
+
+ RoundedCornerWidget
+
+ Radius
+ 圆角
+
+
+ Size / ratio
+ 尺寸/比例
+
+
+ Size
+ 尺寸
+
+
+ Lock ratio
+ 锁定比例
+
+
+ Ratio
+ 比例
+
+
+ Keep the selected size or ratio for the next screenshot
+ 选中后,下次截图保持该尺寸/比例
+
+
diff --git a/translations/deepin-screen-recorder_zh_HK.ts b/translations/deepin-screen-recorder_zh_HK.ts
index 3e22e4b0a..a7069e07a 100644
--- a/translations/deepin-screen-recorder_zh_HK.ts
+++ b/translations/deepin-screen-recorder_zh_HK.ts
@@ -854,4 +854,31 @@ Hold down Shift to draw squares or circles.
離開
+
+ RoundedCornerWidget
+
+ Radius
+ 圓角
+
+
+ Size / ratio
+ 尺寸/比例
+
+
+ Size
+ 尺寸
+
+
+ Lock ratio
+ 鎖定比例
+
+
+ Ratio
+ 比例
+
+
+ Keep the selected size or ratio for the next screenshot
+ 選中後,下次截圖保持該尺寸/比例
+
+
diff --git a/translations/deepin-screen-recorder_zh_TW.ts b/translations/deepin-screen-recorder_zh_TW.ts
index 0b377e9d5..ca8decb85 100644
--- a/translations/deepin-screen-recorder_zh_TW.ts
+++ b/translations/deepin-screen-recorder_zh_TW.ts
@@ -854,4 +854,31 @@ Hold down Shift to draw squares or circles.
離開
+
+ RoundedCornerWidget
+
+ Radius
+ 圓角
+
+
+ Size / ratio
+ 尺寸/比例
+
+
+ Size
+ 尺寸
+
+
+ Lock ratio
+ 鎖定比例
+
+
+ Ratio
+ 比例
+
+
+ Keep the selected size or ratio for the next screenshot
+ 選中後,下次截圖保持該尺寸/比例
+
+