fix: v0.5.1 —— 导出窗口适配屏幕、去掉实时预览字样、图标加底板
- 导出预览窗口按屏幕可用高度计算尺寸,底部的「保存 PDF…」不会再顶出屏幕 - 默认欢迎文档去掉「实时预览(下一版)」,换成已实现的导出 PDF - logo 是黑字透明底,放深色任务栏里等于看不见;packaging/generate_icons.py 给它垫白色圆角底板并输出 16–256 多尺寸 ico,窗口图标与 exe 图标统一使用 - 新增 packaging/generate_icons.py,便于换图后重新生成 - 文档与截图同步
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。
|
一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。
|
||||||
|
|
||||||
当前版本 **0.5.0**。
|
当前版本 **0.5.1**。
|
||||||
|
|
||||||
## 界面
|
## 界面
|
||||||
|
|
||||||
@@ -140,7 +140,9 @@ make -C build install INSTALL_ROOT=/tmp/pkg # 打包用,装到 /tmp/pkg/usr/lo
|
|||||||
|
|
||||||
## 图标
|
## 图标
|
||||||
|
|
||||||
`logo.png` 是源文件,`packaging/` 里是从它生成的各平台图标:
|
`logo.png` 是源文件,`packaging/generate_icons.py` 从它生成各平台图标。
|
||||||
|
源图是**黑色字标 + 透明背景**,直接做成图标放进深色任务栏会“消失”,
|
||||||
|
所以脚本会给它垫一块白色圆角底板,并输出多个尺寸(16–256):
|
||||||
|
|
||||||
| 文件 | 平台 |
|
| 文件 | 平台 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -149,6 +151,7 @@ make -C build install INSTALL_ROOT=/tmp/pkg # 打包用,装到 /tmp/pkg/usr/lo
|
|||||||
| `voiletmark.png` | Linux 桌面图标(hicolor 256×256) |
|
| `voiletmark.png` | Linux 桌面图标(hicolor 256×256) |
|
||||||
|
|
||||||
运行时的窗口图标直接嵌在资源里(`resources.qrc`),所以不安装也有图标。
|
运行时的窗口图标直接嵌在资源里(`resources.qrc`),所以不安装也有图标。
|
||||||
|
改了 `logo.png` 之后跑一次 `python3 packaging/generate_icons.py` 重新生成即可。
|
||||||
|
|
||||||
## 命令行
|
## 命令行
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
TEMPLATE = app
|
TEMPLATE = app
|
||||||
TARGET = VoiletMark
|
TARGET = VoiletMark
|
||||||
VERSION = 0.5.0
|
VERSION = 0.5.1
|
||||||
|
|
||||||
QT += core gui widgets printsupport
|
QT += core gui widgets printsupport
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
@@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""从 logo.png 生成各平台图标。
|
||||||
|
|
||||||
|
logo.png 是黑色字标 + 透明背景:放在深色 Windows 任务栏里会“看不见”。
|
||||||
|
所以先给它加一块白色圆角底板,再导出 ico / icns / png。
|
||||||
|
|
||||||
|
用法:python3 packaging/generate_icons.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
SOURCE = os.path.join(ROOT, "logo.png")
|
||||||
|
OUT_DIR = os.path.join(ROOT, "packaging")
|
||||||
|
|
||||||
|
MASTER = 256
|
||||||
|
SUPERSAMPLE = 2
|
||||||
|
PLATE_RADIUS = 52
|
||||||
|
GLYPH_RATIO = 0.66
|
||||||
|
PLATE_COLOR = (255, 255, 255)
|
||||||
|
BORDER_COLOR = (222, 226, 230)
|
||||||
|
|
||||||
|
SIZES = (16, 24, 32, 48, 64, 128, 256)
|
||||||
|
|
||||||
|
|
||||||
|
def run(args, **kwargs):
|
||||||
|
return subprocess.run(args, check=True, capture_output=True, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def load_rgba(path, size):
|
||||||
|
return run(["ffmpeg", "-v", "error", "-i", path, "-vf",
|
||||||
|
f"scale={size}:{size}:flags=lanczos", "-pix_fmt", "rgba",
|
||||||
|
"-f", "rawvideo", "-"]).stdout
|
||||||
|
|
||||||
|
|
||||||
|
def save_png(path, rgba, size):
|
||||||
|
run(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgba",
|
||||||
|
"-s", f"{size}x{size}", "-i", "-", path], input=rgba)
|
||||||
|
|
||||||
|
|
||||||
|
def rounded_mask(size, radius):
|
||||||
|
"""圆角矩形覆盖率,左上角为 (0,0)。"""
|
||||||
|
mask = bytearray(size * size)
|
||||||
|
r = radius
|
||||||
|
for y in range(size):
|
||||||
|
for x in range(size):
|
||||||
|
dx = min(x, size - 1 - x)
|
||||||
|
dy = min(y, size - 1 - y)
|
||||||
|
if dx >= r or dy >= r:
|
||||||
|
mask[y * size + x] = 255
|
||||||
|
continue
|
||||||
|
ddx = r - dx
|
||||||
|
ddy = r - dy
|
||||||
|
dist = (ddx * ddx + ddy * ddy) ** 0.5
|
||||||
|
if dist <= r:
|
||||||
|
mask[y * size + x] = 255
|
||||||
|
return mask
|
||||||
|
|
||||||
|
|
||||||
|
def compose_master():
|
||||||
|
size = MASTER * SUPERSAMPLE
|
||||||
|
radius = PLATE_RADIUS * SUPERSAMPLE
|
||||||
|
border = 3 * SUPERSAMPLE
|
||||||
|
glyph_size = int(size * GLYPH_RATIO)
|
||||||
|
offset = (size - glyph_size) // 2
|
||||||
|
|
||||||
|
plate = rounded_mask(size, radius)
|
||||||
|
glyph = load_rgba(SOURCE, glyph_size)
|
||||||
|
|
||||||
|
canvas = bytearray(size * size * 4)
|
||||||
|
for y in range(size):
|
||||||
|
for x in range(size):
|
||||||
|
idx = (y * size + x) * 4
|
||||||
|
if not plate[y * size + x]:
|
||||||
|
continue
|
||||||
|
dx = min(x, size - 1 - x)
|
||||||
|
dy = min(y, size - 1 - y)
|
||||||
|
color = BORDER_COLOR if (dx < border or dy < border) else PLATE_COLOR
|
||||||
|
canvas[idx:idx + 4] = bytes((color[0], color[1], color[2], 255))
|
||||||
|
|
||||||
|
for gy in range(glyph_size):
|
||||||
|
for gx in range(glyph_size):
|
||||||
|
src = (gy * glyph_size + gx) * 4
|
||||||
|
alpha = glyph[src + 3] / 255.0
|
||||||
|
if alpha <= 0.0:
|
||||||
|
continue
|
||||||
|
dst = ((gy + offset) * size + (gx + offset)) * 4
|
||||||
|
for channel in range(3):
|
||||||
|
canvas[dst + channel] = int(
|
||||||
|
glyph[src + channel] * alpha + canvas[dst + channel] * (1.0 - alpha))
|
||||||
|
|
||||||
|
# 缩小回 256,顺便得到平滑的边缘
|
||||||
|
return run(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgba",
|
||||||
|
"-s", f"{size}x{size}", "-i", "-", "-vf",
|
||||||
|
f"scale={MASTER}:{MASTER}:flags=lanczos", "-frames:v", "1",
|
||||||
|
"-f", "rawvideo", "-"], input=bytes(canvas)).stdout
|
||||||
|
|
||||||
|
|
||||||
|
def bmp_entry(rgba, size):
|
||||||
|
header = struct.pack("<IiiHHIIiiII", 40, size, size * 2, 1, 32, 0,
|
||||||
|
size * size * 4, 0, 0, 0, 0)
|
||||||
|
rows = []
|
||||||
|
for y in range(size - 1, -1, -1):
|
||||||
|
row = bytearray()
|
||||||
|
for x in range(size):
|
||||||
|
i = (y * size + x) * 4
|
||||||
|
r, g, b, a = rgba[i:i + 4]
|
||||||
|
row += bytes((b, g, r, a))
|
||||||
|
rows.append(bytes(row))
|
||||||
|
|
||||||
|
mask_row_bytes = ((size + 31) // 32) * 4
|
||||||
|
mask = bytearray()
|
||||||
|
for y in range(size - 1, -1, -1):
|
||||||
|
bits = bytearray(mask_row_bytes)
|
||||||
|
for x in range(size):
|
||||||
|
if rgba[(y * size + x) * 4 + 3] < 128:
|
||||||
|
bits[x // 8] |= 0x80 >> (x % 8)
|
||||||
|
mask += bits
|
||||||
|
return header + b"".join(rows) + bytes(mask)
|
||||||
|
|
||||||
|
|
||||||
|
def write_ico(path, images):
|
||||||
|
header = struct.pack("<HHH", 0, 1, len(images))
|
||||||
|
offset = 6 + 16 * len(images)
|
||||||
|
entries = b""
|
||||||
|
payload = b""
|
||||||
|
for size, data in images:
|
||||||
|
dim = 0 if size >= 256 else size
|
||||||
|
entries += struct.pack("<BBBBHHII", dim, dim, 0, 0, 1, 32, len(data), offset)
|
||||||
|
payload += data
|
||||||
|
offset += len(data)
|
||||||
|
with open(path, "wb") as handle:
|
||||||
|
handle.write(header + entries + payload)
|
||||||
|
|
||||||
|
|
||||||
|
def chunk(kind, data):
|
||||||
|
return kind + struct.pack(">I", 8 + len(data)) + data
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.makedirs(OUT_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
master = compose_master()
|
||||||
|
master_png = os.path.join(OUT_DIR, "voiletmark.png")
|
||||||
|
save_png(master_png, master, MASTER)
|
||||||
|
|
||||||
|
pngs = {}
|
||||||
|
master_bytes = open(master_png, "rb").read()
|
||||||
|
for size in SIZES:
|
||||||
|
if size == MASTER:
|
||||||
|
pngs[size] = master_bytes
|
||||||
|
continue
|
||||||
|
temp = os.path.join(OUT_DIR, f"_tmp_{size}.png")
|
||||||
|
run(["ffmpeg", "-y", "-v", "error", "-i", master_png, "-vf",
|
||||||
|
f"scale={size}:{size}:flags=lanczos", "-frames:v", "1", temp])
|
||||||
|
pngs[size] = open(temp, "rb").read()
|
||||||
|
os.remove(temp)
|
||||||
|
|
||||||
|
images = []
|
||||||
|
for size in SIZES:
|
||||||
|
if size == MASTER:
|
||||||
|
images.append((size, pngs[size]))
|
||||||
|
else:
|
||||||
|
images.append((size, bmp_entry(load_rgba(master_png, size), size)))
|
||||||
|
write_ico(os.path.join(OUT_DIR, "voiletmark.ico"), images)
|
||||||
|
|
||||||
|
body = chunk(b"ic07", pngs[128]) + chunk(b"ic08", pngs[256])
|
||||||
|
with open(os.path.join(OUT_DIR, "voiletmark.icns"), "wb") as handle:
|
||||||
|
handle.write(b"icns" + struct.pack(">I", 8 + len(body)) + body)
|
||||||
|
|
||||||
|
print("icons written to", OUT_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 7.6 KiB |
@@ -1,5 +1,5 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<file>logo.png</file>
|
<file>packaging/voiletmark.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <QPageSize>
|
#include <QPageSize>
|
||||||
#include <QPrinter>
|
#include <QPrinter>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QScreen>
|
||||||
#include <QTextBrowser>
|
#include <QTextBrowser>
|
||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
@@ -23,7 +24,10 @@ ExportDialog::ExportDialog(const QString &markdown, const QString &documentName,
|
|||||||
{
|
{
|
||||||
setWindowTitle(tr("导出 PDF — %1")
|
setWindowTitle(tr("导出 PDF — %1")
|
||||||
.arg(documentName.isEmpty() ? tr("未命名") : documentName));
|
.arg(documentName.isEmpty() ? tr("未命名") : documentName));
|
||||||
resize(900, 840);
|
|
||||||
|
// 别让窗口比屏幕还高,否则底部的按钮会顶到屏幕外面去。
|
||||||
|
const QRect available = screen() ? screen()->availableGeometry() : QRect(0, 0, 1280, 900);
|
||||||
|
resize(qMin(900, available.width() - 80), qMin(860, available.height() - 100));
|
||||||
|
|
||||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||||
layout->setContentsMargins(16, 16, 16, 12);
|
layout->setContentsMargins(16, 16, 16, 12);
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const char kWelcomeText[] = R"VOILETMARK(
|
|||||||
|
|
||||||
- [x] 自动深浅色主题
|
- [x] 自动深浅色主题
|
||||||
- [x] Markdown 语法高亮
|
- [x] Markdown 语法高亮
|
||||||
- [ ] 实时预览(下一版)
|
- [x] 导出 PDF(带预览)
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -671,7 +671,7 @@ void MainWindow::showAbout()
|
|||||||
{
|
{
|
||||||
QMessageBox box(this);
|
QMessageBox box(this);
|
||||||
box.setWindowTitle(tr("关于 VoiletMark"));
|
box.setWindowTitle(tr("关于 VoiletMark"));
|
||||||
box.setIconPixmap(QPixmap(QStringLiteral(":/logo.png"))
|
box.setIconPixmap(QPixmap(QStringLiteral(":/packaging/voiletmark.png"))
|
||||||
.scaled(72, 72, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
.scaled(72, 72, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||||
box.setTextFormat(Qt::RichText);
|
box.setTextFormat(Qt::RichText);
|
||||||
box.setText(tr("<h3>VoiletMark %1</h3>"
|
box.setText(tr("<h3>VoiletMark %1</h3>"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
QApplication::setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
|
QApplication::setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
|
||||||
QApplication::setWindowIcon(QIcon(QStringLiteral(":/logo.png")));
|
QApplication::setWindowIcon(QIcon(QStringLiteral(":/packaging/voiletmark.png")));
|
||||||
QGuiApplication::setDesktopFileName(QStringLiteral("voiletmark"));
|
QGuiApplication::setDesktopFileName(QStringLiteral("voiletmark"));
|
||||||
|
|
||||||
QCoreApplication::setOrganizationName(QStringLiteral("VoiletMark"));
|
QCoreApplication::setOrganizationName(QStringLiteral("VoiletMark"));
|
||||||
|
|||||||