• 使用Qt轻量的QTextBrowser为taskBus SDR显示丰富的图文帮助


    最近准备为taskBus软件无线电平台添加集成文档功能。毕竟模块变多了之后,简单一页纸的小文档对正确想起来如何使用模块功能意义很大。对于一款极简的轻量级SDR产品,我们希望taskBus的文档继续保持极简的风格,支持txt纯文本、简易Markdown和本地html。虽然以前在别的项目里,用QtWebKit显示HTML,后来用过QtWebEngine,但有问题一直没有解决。首先这两个基于浏览器内核的库都太大了,另外Qt WebEngine不支持mingw编译器。

    早就听说Qt的textBrowser是一种轻量级的超文本浏览器,类似的控件是QTextEdit,还具备编辑功能。项目用QTextBrowser显示简单的html文件、图文提示、帮助,还支持markdown,又不会带来额外的依赖。本文介绍一下我尝试用QTextBrowser显示Markdown+Html文件的情况。

    1. 途径比较

    要在Qt程序里打开Markdown/Html文件,至少有三种方法。

    • 浏览器显示:要显示渲染好的Markdown文件,最常用的是调用js脚本和浏览器,直接chrome打开。这种方法对markdown的支持最灵活完善。
    • Qt WebEngine/WebKit 内嵌网页+js显示:和浏览器类似,只是可以自然地嵌入到自己的代码里,不用弹出浏览器(通过一定的平台相关的API,浏览器窗口也可以嵌入到app里,只是没有原生的方便)。
    • QTextBrowser显示:最轻量级,不需要引入浏览器、WebEngine/Webkit,特别适用于简单的小帮助文档显示。

    这三种方法里,QTextBrowser是费效比最好的,但是对Markdown的支持比较弱,对转义符的处理也与JS前端常见行为有所区别。

    2. 实现原则

    一般的带模块扩展功能的软件,模块的文档有两种管理方式。其一是集约的,其二是分散的。集约的方式,所有模块的可执行统一位于可执行文件夹,文档位于文档文件夹。分散的,则是一个模块的可执行、文档等东西放在一起。

    相比这两种方式,对于小作坊的绿色软件而言,分散管理便于给模块开发者更大的简便性。开发者只要把模块的东西丢在一起,就够了。要卸载模块,直接把文件夹删除,就卸载了。

    为了最简化我们的设计思路,固定文档规则如下:

    • 摈弃靠环境变量设置来定位文档的思路。文档始终相对于模块的路径不变。
    • 文档可以只包含一个文件。只包含一个文件的文档,应该与模块的可执行文件在一个文件夹。
    • 文档可以包含图文。如果这个模块的文档包含图文,则应该集中放在与模块同名的handbook文件夹下。这样图文等零碎文件不会污染上级文件夹。
    • 文档的入口名称是固定的,恒定为模块可执行名称+txt|md|html等扩展名。
    • html优先级最高,其次是md,再次txt

    有了这样的规则,在框架程序内可方便的查找文档。假设模块的名字是 mod.exe,则文档的合法位置、查找顺序如下:

    |
    +-mod_root
       |-0 mod.exe(模块可执行)
       |-1 mod.exe.html
       |-2 mod.html
       |-4 mod.exe.md
       |-5 mod.md
       |-7 mod.exe.txt
       |-8 mod.txt
       +-mod.handbook
         |-3 mod.html
         |-6 mod.md
         |-9 mod.txt
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    按照上述顺序,先找到哪个文件,就打开谁。

    3 实现过程

    3.1 建立视图

    新建一个视图,用于盛放帮助文档。

    视图
    这个视图存在一个setUrl方法,用于打开相应的磁盘文件。

    • 注意,本地文件要转换为file://开始的url格式。
    • 为了保持一致性,如果相关帮助主题不存在,要显示友好的默认页面。
    • QTextBrowser::setSource会自动判断文档类型。
    HandbookView::HandbookView(QWidget *parent) :
    	QWidget(parent),
    	ui(new Ui::HandbookView)
    {
    	ui->setupUi(this);
    	connect (ui->textBrowser_help, &QTextBrowser::backwardAvailable,ui->pushButton_backward,&QPushButton::setEnabled);
    	connect (ui->textBrowser_help, &QTextBrowser::forwardAvailable,ui->pushButton_forward,&QPushButton::setEnabled);
    	connect (ui->pushButton_forward,&QPushButton::pressed,ui->textBrowser_help, &QTextBrowser::forward);
    	connect (ui->pushButton_backward,&QPushButton::pressed,ui->textBrowser_help, &QTextBrowser::backward);
    	connect (ui->textBrowser_help, &QTextBrowser::sourceChanged,[&](QUrl u){
    		ui->lineEdit_url->setText(u.toDisplayString());
    	});
    
    }
    void HandbookView::setUrl(QString urlstr,QString FailedString)
    {
    	if (urlstr.length())
    	{
    		QUrl u = QUrl::fromLocalFile(urlstr);
    		ui->textBrowser_help->setSource(u) ;
    	}
    	else
    	{
    		ui->textBrowser_help->setMarkdown(
    					tr("# Documents Does NOT Exist\r\n\r\n"
    					"You can write documents with markdown in any place below:\r\n\r\n")
    					+FailedString
    					);
    	}
    }
    void HandbookView::on_lineEdit_url_returnPressed()
    {
    	QUrl u = QUrl(ui->lineEdit_url->text());
    	ui->textBrowser_help->setSource(u);
    }
    
    
    • 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

    下图是文档不存在时的提示:

    在这里插入图片描述

    3.2 从程序里定位文档位置

    按照上述设计思路,直接根据exe文件名,对文档进行查找。

    //通可执行文件名直接查找文档。
    void  taskBusPlatformFrm::load_doucment(QString func,QString exe)
    {
    	//Open handbook
    	QFileInfo info(exe);
    	QString baseName = info.path()+"/"+
    			info.completeBaseName();
    	QString urlstr;
    	//用于加载失败时的提示。
    	QString FailedString;
    	FailedString += exe+".md/.html/.txt" + "\n\n";
    	FailedString += baseName+".md/.html/.txt" + "\n\n";
    	FailedString += baseName+".handbook/" + info.completeBaseName() + ".md/.html/.txt\n\n";
    	//逐一查找文件
    	if (QFileInfo::exists(exe+".html"))
    		urlstr = exe+".html";
    	else if (QFileInfo::exists(baseName+".html"))
    		urlstr = baseName+".html";
    	else if (QFileInfo::exists(baseName+".handbook/" + info.completeBaseName() + ".html"))
    		urlstr = baseName+".handbook/" + info.completeBaseName() + ".html";
    	else if (QFileInfo::exists(exe+".md"))
    		urlstr = exe+".md";
    	else if (QFileInfo::exists(baseName+".md"))
    		urlstr = baseName+".md";
    	else if (QFileInfo::exists(baseName+".handbook/" + info.completeBaseName() + ".md"))
    		urlstr = baseName+".handbook/" + info.completeBaseName() + ".md";
    	else if (QFileInfo::exists(exe+".txt"))
    		urlstr = exe+".txt";
    	else if (QFileInfo::exists(baseName+".txt"))
    		urlstr = baseName+".txt";
    	else if (QFileInfo::exists(baseName+".handbook/" + info.completeBaseName() + ".txt"))
    		urlstr = baseName+".handbook/" + info.completeBaseName() + ".txt";
    	
    	//...
    		HandbookView * view  = new HandbookView(this);
    		if (view)
    		{
    			view->setWindowTitle(title);
    			wnd = ui->mdiArea->addSubWindow(view);
    			view->show();
    			view->setUrl(urlstr,FailedString);
    			wnd->setWindowTitle(title);
    		}
    	//...
    
    }
    
    
    
    • 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

    3.3 在makefile里发布文档

    从代码内做好准备后,还要把源码文件夹里的文档及时拷贝到exe所在文件夹。这里我们使用POST_LINK步骤直接拷贝,而非install步骤。因为大部分受众是绿色软件安装者方式,不希望污染到其他路径。POST_LINK只是告诉用户,发布时应该携带这个文件/文件夹而已。用户开心时,自己手工拷贝也不是不可以。

    #QMAKE:
    DESTDIR = $$OUT_PWD/../../../bin/modules
    #Documents Copy
    QMAKE_POST_LINK += $${QMAKE_COPY_DIR} $$PWD/network_p2p.handbook $$DESTDIR/network_p2p.handbook
    
    • 1
    • 2
    • 3
    • 4
    add_custom_command(TARGET network_p2p
    	     POST_BUILD
    	     COMMAND echo Copy Handbook to EXE path
    	     COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/network_p2p.handbook ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/network_p2p.handbook
    	 )
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在模块代码文件夹,文档存放在 target.handbook文件夹里。这个文件夹会在编译链接后,被拷贝到exe所在文件夹。

    EXE文件夹看起来类似这样:

    在这里插入图片描述

    4. 链接与锚点

    只要保证文档中的图片等依赖是本地的,且在相对路径下存在,则可以直接跳转到链接。同时,QTextBrowser会记住访问历史,并根据前进后退命令进行切换。

    同时,QTextBrower支持在url里使用锚点,跳转到文档的某个位置:

    链接与锚点

    5 文档的撰写与生成

    QTextBrowser 对Markdown的支持不如html好。我的方法是:

    1. 用标准Markdown编辑器撰写。这里推荐Haroopad. 此时可以预览相对路径和资源的可用性。
    2. 另存为html,并携带资源。此时,TOC会展开为文件夹,且支持锚点。

    Haroopad


    相关代码见Git仓库

  • 相关阅读:
    数据库连接池之c3p0-0.9.1.2,线上偶发APPARENT DEADLOCK,如何解?
    (DXE_DRIVER)PciHostBridge
    872. 最大公约数(史上最详细讲解 7种算法,STL+算法标准实现)
    运维开发详解
    详解MySQL information_schema数据库常用的表信息以及各表对应的字段信息;以及如何登录mysql和创建视图
    诈骗分子投递“大闸蟹礼品卡”,快递公司如何使用技术手段提前安全预警?
    Java的面向对象思想
    Flutter release打包安卓闪退,但是ios正常,debug两者都正常
    戴建业老师对李白和杜甫的讨论
    【毕业设计】基于Vue与SSM的在线聊天系统
  • 原文地址:https://blog.csdn.net/goldenhawking/article/details/127686461