Andrew Moa Blog Site

Rust图形界面库初探

最近打算用rust把以前写的代码重构一下,涉及到GUI界面怎么选择的问题。rust正式发布不过十年光景,在GUI开发这方面还不如老牌的C/C++,有诸如wxWidgets、qt、gtk+等众多知名又久经考验的GUI界面库。本文选取几款rust的GUI库,简单实现一个边界层计算器,做个横向对比。

1. slint

slint 近期的宣传不可谓不卖力,号称要打造成下一代gui工具包,看来野心不小。slint通过自定义的声明式语言定义ui界面,在vscode下编程可以通过插件预览,也可以通过官方的slintpad 网站预览。

cargo配置文件如下,slint-build用于将.slint界面文件翻译成.rs文件。

[package]
name = "sLayers-rs"
version = "0.1.0"
edition = "2024"

[dependencies]
slint = "1.8.0"

[build-dependencies]
slint-build = "1.8.0"

[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"

ui界面文件dialog.slint。slint可以把需要调用的参数定义到ui界面中,并自动隐式生成set_get_方法用于在rs文件中设置这些参数的值。同样的,界面中定义的回调函数也会自动隐式生成on_方法,用于在rs文件中调用。需要注意的是,在rs中更新了ui界面中的参数,关联的控件并不会自动更新显示,需要手动更新控件显示。

import { Button, LineEdit, SpinBox, CheckBox, GridBox, VerticalBox } from "std-widgets.slint";

export component Dialog inherits Window { title: "Layers"; in-out property <string> tt: "3.0"; in-out property <int> nl: 5; in-out property <string> ft: "0.3"; in-out property <string> gr: "1.5"; callback calculate_first_thickness(); callback calculate_num_layers(); callback calculate_growth_rate(); callback calculate_total_thickness();

callback calculate_value();
VerticalBox {
    Text {
        text: &#34;Calculate fluid boundary layer parameters.\nCalculate the selected parameters based on the others.&#34;;
    }

    GridBox {
        Row {
            b_t := CheckBox {
                text: &#34;Total thickness (mm)&#34;;
                checked: true;
                enabled: !self.checked;
                toggled() =&gt; {
                    if self.checked {
                        b_n.checked = false;
                        b_f.checked = false;
                        b_g.checked = false;
                    }
                }
            }

            e_t := LineEdit {
                text: root.tt;
                input-type: decimal;
                read-only: b_t.checked;
            }
        }

        Row {
            b_n := CheckBox {
                text: &#34;Number of layers&#34;;
                checked: false;
                enabled: !self.checked;
                toggled() =&gt; {
                    if self.checked {
                        b_t.checked = false;
                        b_f.checked = false;
                        b_g.checked = false;
                    }
                }
            }

            e_n := SpinBox {
                value: root.nl;
                minimum: 1;
            }
        }

        Row {
            b_f := CheckBox {
                text: &#34;First thickness (mm)&#34;;
                checked: false;
                enabled: !self.checked;
                toggled() =&gt; {
                    if self.checked {
                        b_n.checked = false;
                        b_t.checked = false;
                        b_g.checked = false;
                    }
                }
            }

            e_f := LineEdit {
                text: root.ft;
                input-type: decimal;
                read-only: b_f.checked;
            }
        }

        Row {
            b_g := CheckBox {
                text: &#34;Growth rate&#34;;
                checked: false;
                enabled: !self.checked;
                toggled() =&gt; {
                    if self.checked {
                        b_n.checked = false;
                        b_f.checked = false;
                        b_t.checked = false;
                    }
                }
            }

            e_g := LineEdit {
                text: root.gr;
                input-type: decimal;
                read-only: b_g.checked;
            }
        }
    }

    Button {
        text: &#34;Calculate&#34;;
        clicked =&gt; {
            root.tt = e_t.text;
            root.nl = e_n.value;
            root.ft = e_f.text;
            root.gr = e_g.text;
            if b_t.checked {
                root.calculate_total_thickness();
            } else if b_n.checked {
                root.calculate_num_layers();
            } else if b_f.checked {
                root.calculate_first_thickness();
            } else if b_g.checked {
                root.calculate_growth_rate();
            }
            e_t.text = root.tt;
            e_g.text = root.gr;
            e_f.text = root.ft;
            e_n.value = root.nl;
        }
    }
}

}

build.rs构建脚本调用slint-build.slint文件翻译成.rs文件。

阅读时长9分钟
Andrew Moa

一款基于Qt6的图片base64转换工具

经常写Markdown的小伙伴都会遇到一个问题,那就是图片存储问题。Markdown本身并不支持图片内置,传统用法一般通过外链引用本机或网络上的图片。但是当文章在网络上发表时,存储图片就成了个问题,虽然可以通过图床上传,但是操作麻烦。

好在Markdown支持通过base641编码数据渲染图片,这里根据这个功能编写了一个小工具Image2Base64 ,可以方便地在图片文件和base64编码之间转换。

1. 软件功能

下面就是这个软件的图形界面,这个截图就是通过base64渲染的。

4cdbc8106d0296e6261e6abdfa0b0096

软件界面左侧窗口是图片显示窗口,可以通过拖放打开图片,支持bmp、png、jpg等主流格式的图片(暂不支持gif动图显示),支持右键菜单,支持图片放大缩小和重置等基本操作。

左侧窗口下方的Paste按钮可以将剪贴板中的图像数据读取并显示出来,方便通过截图软件和剪贴板交互读取图像数据。

右侧窗口是文本显示窗口,如果转换成功这里会图片对应的base64编码并显示出来。下方的Markdown复选框用于添加Markdown的图片语法标签,点击Copy按钮后可以将文本数据复制到剪贴板,之后就可以直接在MD文件中粘贴了。

值得注意的是,Copy按钮左侧的组合框可以选择base64对应的图片格式。没错,base64也是有对应格式区分的,base64本质上是将二进制编码的文本化,因此不同图片原始格式的大小所导致的base64编码长度自然不一样,甚至图片的复杂程度和压缩比等因素对base64编码长度都有影响。

右侧窗口下方的Save as按钮支持将图片文件以原始格式或base64编码(txt文件)存储。通过中间的两个方向按钮可以实现将图片转换成base64编码,或者将base64编码转换成图片。需要注意的是,将base64编码转换成图片时,请不要包含markdown的语法标签,否则会报错。

该软件经过测试,可以使用png格式base64编码,通过markdown语法标签在marktextjoplin 上渲染图片,其他格式不保证能成功,取决于markdown编辑器的渲染引擎。对于一般屏幕截图,推荐采用png格式。

2. 代码解析

2.1 C++实现

该软件基于Qt6实现,图片格式编码、渲染以及base64转换都是通过Qt实现的。

图片显示通过QLabel实现,重载了QLabel类2,并做了一些调整。

photolabel.h

#ifndef PHOTOLABEL_H
#define PHOTOLABEL_H

#include <QObject>
#include <QLabel>
#include <QMenu>
#include <QDragEnterEvent>
#include <QDropEvent>

class PhotoLabel : public QLabel
{
	Q_OBJECT

public:
	explicit PhotoLabel(QWidget* parent = nullptr);

	void openFile(QString);     //打开图片文件
	void clearShow();           //清空显示
	void setImage(QImage&);     //设置图片
	void openAction();          //调用打开文件对话框
	void pasteAction();         //粘贴来自剪贴板的图片
	const QImage& getImage();   //调用存储的图片数据

signals:
	// 图片加载成功信号
	void imageLoadSuccess();

protected:
	void contextMenuEvent(QContextMenuEvent* event) override;   //右键菜单
	void paintEvent(QPaintEvent* event) override;               //QPaint画图
	void wheelEvent(QWheelEvent* event) override;               //鼠标滚轮滚动
	void mousePressEvent(QMouseEvent* event) override;          //鼠标摁下
	void mouseMoveEvent(QMouseEvent* event) override;           //鼠标松开
	void mouseReleaseEvent(QMouseEvent* event) override;        //鼠标发射事件
	//拖放文件
	void dragEnterEvent(QDragEnterEvent* event) override;
	void dragMoveEvent(QDragMoveEvent* event) override;
	void dropEvent(QDropEvent* event) override;

private slots:
	void initWidget();      //初始化
	void onSelectImage();   //选择打开图片
	void onPasteImage();    //选择粘贴图片
	void onZoomInImage();   //图片放大
	void onZoomOutImage();  //图片缩小
	void onPresetImage();   //图片还原

private:
	QImage m_image;           //显示的图片
	qreal m_zoomValue = 1.0;  //鼠标缩放值
	int m_xPtInterval = 0;    //平移X轴的值
	int m_yPtInterval = 0;    //平移Y轴的值
	QPoint m_oldPos;          //旧的鼠标位置
	bool m_pressed = false;   //鼠标是否被摁压
	QString m_localFileName;  //文件名称
	QMenu* m_menu;            //右键菜单
};

#endif // PHOTOLABEL_H

photolabel.cpp

阅读时长7分钟
Andrew Moa