feat: v0.10.1 —— PDF 里的代码块渲染成一整块带边框的面板

- 导出时不再逐行铺底色,改用 QTextFrame 把整段代码包在一个框里:
  统一背景、1px 边框、内边距,跨页也保持连续
- 面板右上角标注语言名(从围栏 info string 取,cpp → C++,没有标记则不显示)
- 行内代码颜色从强调红改为正文色,只保留灰底,跟编辑器一致
- 文档开头直接是代码块、以及无语言标记的情况都验证过
- 版本 0.10.1
This commit is contained in:
2026-10-22 18:34:37 +08:00
parent 0a6a3f8775
commit 742bbf4a2b
3 changed files with 67 additions and 20 deletions
+4 -1
View File
@@ -2,7 +2,7 @@
一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。 一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。
当前版本 **0.10.0**。 当前版本 **0.10.1**。
## 界面 ## 界面
@@ -96,6 +96,9 @@ A4 纸、16mm 页边距。
支持标题、列表(含任务列表)、引用、代码块、行内代码、粗体 / 斜体 / 删除线、 支持标题、列表(含任务列表)、引用、代码块、行内代码、粗体 / 斜体 / 删除线、
链接;图片只输出 alt 文字,表格暂时按原文输出。 链接;图片只输出 alt 文字,表格暂时按原文输出。
代码块在 PDF 里是**一整块带边框的面板**(不是一行一条底色),
右上角标注语言名(C++ / Python …,取围栏上的标记)。
### 语言 ### 语言
界面支持中文和英文,在 **视图 → 语言** 里切换,立即生效、不需要重启。 界面支持中文和英文,在 **视图 → 语言** 里切换,立即生效、不需要重启。
+1 -1
View File
@@ -1,6 +1,6 @@
TEMPLATE = app TEMPLATE = app
TARGET = VoiletMark TARGET = VoiletMark
VERSION = 0.10.0 VERSION = 0.10.1
QT += core gui widgets printsupport QT += core gui widgets printsupport
+62 -18
View File
@@ -1,10 +1,13 @@
#include "MarkdownRenderer.h" #include "MarkdownRenderer.h"
#include "CodeHighlight.h"
#include <QColor> #include <QColor>
#include <QFont> #include <QFont>
#include <QRegularExpression> #include <QRegularExpression>
#include <QTextBlockFormat> #include <QTextBlockFormat>
#include <QTextCharFormat> #include <QTextCharFormat>
#include <QTextFrame>
#include <QTextCursor> #include <QTextCursor>
#include <QTextDocument> #include <QTextDocument>
@@ -37,8 +40,9 @@ struct Style
QColor heading = hex("#11151a"); QColor heading = hex("#11151a");
QColor muted = hex("#5b6570"); QColor muted = hex("#5b6570");
QColor rule = hex("#d0d7de"); QColor rule = hex("#d0d7de");
QColor codeText = hex("#b4295a"); QColor codeText = hex("#24292f");
QColor codeBg = hex("#f3f4f6"); QColor codeBg = hex("#f3f4f6");
QColor border = hex("#d0d7de");
QColor link = hex("#0969da"); QColor link = hex("#0969da");
QColor marker = hex("#0969da"); QColor marker = hex("#0969da");
}; };
@@ -62,7 +66,7 @@ private:
void renderHeading(int level, const QString &text); void renderHeading(int level, const QString &text);
void renderParagraph(const QString &text, int quoteDepth); void renderParagraph(const QString &text, int quoteDepth);
void renderListItem(const QString &marker, const QString &text, int indentLevel); void renderListItem(const QString &marker, const QString &text, int indentLevel);
void renderCodeBlock(const QStringList &lines); void renderCodeBlock(const QStringList &lines, const QString &info);
void renderRule(); void renderRule();
void renderInline(const QString &text, const QTextCharFormat &base); void renderInline(const QString &text, const QTextCharFormat &base);
@@ -158,29 +162,69 @@ void Renderer::renderListItem(const QString &marker, const QString &text, int in
renderInline(text, bodyFormat()); renderInline(text, bodyFormat());
} }
void Renderer::renderCodeBlock(const QStringList &lines) void Renderer::renderCodeBlock(const QStringList &lines, const QString &info)
{ {
QTextBlockFormat blockFormat; if (m_firstBlock) {
blockFormat.setLineHeight(135, QTextBlockFormat::ProportionalHeight); // 文档一开头就是代码块:先垫一个空段落,frame 才有地方插。
blockFormat.setTopMargin(6); QTextBlockFormat emptyFormat;
blockFormat.setBottomMargin(10); beginBlock(emptyFormat, bodyFormat());
blockFormat.setLeftMargin(14); }
blockFormat.setRightMargin(14);
blockFormat.setBackground(m_style.codeBg);
QTextFrameFormat frameFormat;
frameFormat.setBackground(m_style.codeBg);
frameFormat.setBorder(1);
frameFormat.setBorderBrush(m_style.border);
frameFormat.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
frameFormat.setPadding(8);
frameFormat.setTopMargin(6);
frameFormat.setBottomMargin(12);
QTextFrame *frame = m_cursor.insertFrame(frameFormat);
QTextCursor frameCursor(m_document);
frameCursor.setPosition(frame->firstPosition());
bool firstInside = true;
const auto insideBlock = [&](const QTextBlockFormat &blockFormat,
const QTextCharFormat &charFormat) {
if (firstInside) {
frameCursor.setBlockFormat(blockFormat);
frameCursor.setBlockCharFormat(charFormat);
firstInside = false;
} else {
frameCursor.insertBlock(blockFormat, charFormat);
}
};
const QString language = CodeHighlight::displayName(CodeHighlight::fromInfoString(info));
if (!language.isEmpty()) {
QTextBlockFormat labelFormat;
labelFormat.setAlignment(Qt::AlignRight);
labelFormat.setBottomMargin(2);
QTextCharFormat labelChar;
labelChar.setForeground(m_style.muted);
labelChar.setFontPointSize(kBodySize * 0.8);
insideBlock(labelFormat, labelChar);
frameCursor.insertText(language, labelChar);
}
QTextBlockFormat codeBlock;
codeBlock.setLineHeight(130, QTextBlockFormat::ProportionalHeight);
const QTextCharFormat format = codeFormat(); const QTextCharFormat format = codeFormat();
QStringList content = lines; QStringList content = lines;
if (content.isEmpty()) if (content.isEmpty())
content << QString(); content << QString();
for (int index = 0; index < content.size(); ++index) { for (const QString &line : content) {
QTextBlockFormat lineFormat = blockFormat; insideBlock(codeBlock, format);
lineFormat.setTopMargin(index == 0 ? 6 : 0); frameCursor.insertText(line.isEmpty() ? QStringLiteral(" ") : line, format);
lineFormat.setBottomMargin(index == content.size() - 1 ? 10 : 0);
beginBlock(lineFormat, format);
const QString line = content.at(index);
m_cursor.insertText(line.isEmpty() ? QStringLiteral(" ") : line, format);
} }
// 光标挪到边框外面,后面的内容接着写
m_cursor = QTextCursor(m_document);
m_cursor.setPosition(frame->lastPosition());
} }
void Renderer::renderRule() void Renderer::renderRule()
@@ -329,7 +373,7 @@ void Renderer::run(const QString &markdown)
code << lines.at(index++); code << lines.at(index++);
if (index < lines.size()) if (index < lines.size())
++index; // 跳过闭合围栏 ++index; // 跳过闭合围栏
renderCodeBlock(code); renderCodeBlock(code, fence.captured(1));
continue; continue;
} }