用 Turbo Vision 2 为 Qt 6 控制台应用创建 TUI 字符 MainFrame

Qt是非常好的C++开发框架,虽然对没有GUI的操作系统也提供了platform插件,如 vnc, framebuf等,但是终究不是真正意义上的字符化的界面。以前在字符模式下,Qt只能用curse自己画对话框。但是自己画对话框毕竟不是一种省事的方式。

想到我在1996年似乎使用过Borland Turbo C++提供的视觉库"Turbo-vision", 印象深刻,通过搜索,这个Turbo Vision已经成为开源项目,正好可以移植到Qt来用!一如既往,本次实验我们还在msys2 Qt ucrt64 环境下来做。

1. 下载并编译 Turbo Vision

bash 复制代码
 git clone https://github.com/magiblot/tvision.git
 cd tvision/
 cmake . -B ./build/ucrt64 -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release && cmake --build ./build/ucrt64

如果需要参考教材,可以继续克隆教材库:

bash 复制代码
cd ..
git clone  https://github.com/yym36100/t3_rom.git

编译完毕后,可以直接进入文件夹运行例子。这里不再赘述。教材很厚,非常佩服以前的纸质教材:

编译好后,发现库是静态的!太棒了。

2. 从Editor例子构造Qt应用

我们从TVisition的例子入手,构造Qt应用。下面完整附加所有代码。

2.1 工程文件

Qt控制台程序,6.9.1,文件名为qtvedit.pro

bash 复制代码
QT = core concurrent
CONFIG += c++17 cmdline

HEADERS += \
    editor/tvedit.h
SOURCES += \
        editor/tvedit1.cpp \
        editor/tvedit2.cpp \
        editor/tvedit3.cpp \
        main.cpp

TVISION_PATH = c:/msys64/home/user/projects/3rdparty/tvision
INCLUDEPATH += $$TVISION_PATH/include
#Only for this example
INCLUDEPATH += $$TVISION_PATH/include/tvision/compat/borland
LIBS += -L$$TVISION_PATH/build/ucrt64 -ltvision

以tv开始的文件就是Turbo Vision自带的例子改的文件,main.cpp是我们Qt的文件。

2.2 使用独立的线程运行TVision事件循环

如果仔细看TVision的例子,就会发现它的main和Qt的很像,都要有一个全局事件泵(App)来运行。这样的话,原则上和Qt的Application是冲突的。怎么办?当然可以多线程了。让Qt在主线程,TApp在子线程,文件名为main.cpp:

cpp 复制代码
#include <QCoreApplication>
#include <QtConcurrent>
int run_tvmain(int argc, char **argv);

int main(int argc, char *argv[])
{
	QCoreApplication a(argc, argv);
	//子线程运行Turbo Vision,并优雅退出
	auto fu = QtConcurrent::run([&]() -> int {
				  run_tvmain(argc, argv);
				  return 0;
			  }).then([](int ret) { QCoreApplication::exit(ret); });

	return a.exec();
}

我们改造 tedit1.cpp里的main函数:

cpp 复制代码
int run_tvmain(int argc, char **argv)
{
	TEditorApp *editorApp = new TEditorApp(argc, argv);
	editorApp->run();
	editorApp->deleteLater();
	return 0;
}

这样就实现了两套App 运行。

2.3 使用QObject多重派生TApp

我们使用QObject作为基类,多重派生TApp,使得它可以支持信号和槽以及Meta,文件名为tvedit.h:

cpp 复制代码
#if !defined(__TVEDIT_H)
#define __TVEDIT_H
#define Uses_TApplication
#define Uses_TEditWindow
#define Uses_TDeskTop
#define Uses_TRect
#define Uses_TEditor
#define Uses_TFileEditor
#define Uses_TFileDialog
#define Uses_TChDirDialog
#define Uses_TDialog
#define Uses_TProgram
#define Uses_TObject
#define Uses_TInputLine
#define Uses_TLabel
#define Uses_THistory
#define Uses_TCheckBoxes
#define Uses_TButton
#define Uses_MsgBox
#define Uses_TSItem
#define Uses_TMenuBar
#define Uses_TSubMenu
#define Uses_TKeys
#define Uses_TMenuItem
#define Uses_TStatusLine
#define Uses_TStatusItem
#define Uses_TStatusDef
#define Uses_TPoint

#include <QObject>
#include <iomanip.h>
#include <stdlib.h>
#include <strstrea.h>
#include <tvision/tv.h>

class TMenuBar;
class TStatusLine;
class TEditWindow;
class TDialog;

const int cmChangeDrct = 102;

class TEditorApp : public QObject, public TApplication
{
	Q_OBJECT
public:
	TEditorApp(int argc, char **argv, QObject *parent = nullptr);
	virtual void handleEvent(TEvent &event);
	static TMenuBar *initMenuBar(TRect);
	static TStatusLine *initStatusLine(TRect);
	virtual void outOfMemory();

private:
	TEditWindow *openEditor(const char *fileName, Boolean visible);
	void fileOpen();
	void fileNew();
	void changeDir();
};

ushort execDialog(TDialog *d, void *data);
TDialog *createFindDialog();
TDialog *createReplaceDialog();
ushort doEditDialog(int dialog, ...);

#endif // __TVEDIT_H

2.4 改造后的 tvedit1.cpp

cpp 复制代码
#include "tvedit.h"
TEditWindow *TEditorApp::openEditor(const char *fileName, Boolean visible)
{
	TRect r = deskTop->getExtent();
	TView *p = validView(new TEditWindow(r, fileName, wnNoNumber));
	if (!visible)
		p->hide();
	deskTop->insert(p);
	return (TEditWindow *) p;
}
TEditorApp::TEditorApp(int argc, char **argv, QObject *parent)
	: QObject(parent)
	, TProgInit(TEditorApp::initStatusLine, TEditorApp::initMenuBar, TEditorApp::initDeskTop)
	, TApplication()
{
	TCommandSet ts;
	ts.enableCmd(cmSave);
	ts.enableCmd(cmSaveAs);
	ts.enableCmd(cmCut);
	ts.enableCmd(cmCopy);
	ts.enableCmd(cmPaste);
	ts.enableCmd(cmClear);
	ts.enableCmd(cmUndo);
	ts.enableCmd(cmFind);
	ts.enableCmd(cmReplace);
	ts.enableCmd(cmSearchAgain);
	disableCommands(ts);

	TEditor::editorDialog = doEditDialog;

	while (--argc > 0)			   // Open files specified
		openEditor(*++argv, True); // on command line.
	cascade();
}

void TEditorApp::fileOpen()
{
	char fileName[4096];
	strcpy(fileName, "*.*");

	if (execDialog(new TFileDialog("*.*", "Open file", "~N~ame", fdOpenButton, 100), fileName)
		!= cmCancel)
		openEditor(fileName, True);
}

void TEditorApp::fileNew()
{
	openEditor(0, True);
}

void TEditorApp::changeDir()
{
	execDialog(new TChDirDialog(cdNormal, 0), 0);
}

void TEditorApp::handleEvent(TEvent &event)
{
	TApplication::handleEvent(event);
	if (event.what != evCommand)
		return;
	else
		switch (event.message.command)
		{
		case cmOpen:
			fileOpen();
			break;

		case cmNew:
			fileNew();
			break;

		case cmChangeDrct:
			changeDir();
			break;

		default:
			return;
		}
	clearEvent(event);
}

int run_tvmain(int argc, char **argv)
{
	TEditorApp *editorApp = new TEditorApp(argc, argv);
	editorApp->run();
	editorApp->deleteLater();
	return 0;
}

2.5 tvedit2.cpp

cpp 复制代码
#include "tvedit.h"

ushort execDialog(TDialog *d, void *data)
{
	TView *p = TProgram::application->validView(d);
	if (p == 0)
		return cmCancel;
	else
	{
		if (data != 0)
			p->setData(data);
		ushort result = TProgram::deskTop->execView(p);
		if (result != cmCancel && data != 0)
			p->getData(data);
		TObject::destroy(p);
		return result;
	}
}

TDialog *createFindDialog()
{
	TDialog *d = new TDialog(TRect(0, 0, 38, 12), "Find");

	d->options |= ofCentered;

	TInputLine *control = new TInputLine(TRect(3, 3, 32, 4), 80);
	d->insert(control);
	d->insert(new TLabel(TRect(2, 2, 15, 3), "~T~ext to find", control));
	d->insert(new THistory(TRect(32, 3, 35, 4), control, 10));

	d->insert(new TCheckBoxes(TRect(3, 5, 35, 7),
							  new TSItem("~C~ase sensitive", new TSItem("~W~hole words only", 0))));

	d->insert(new TButton(TRect(14, 9, 24, 11), "O~K~", cmOK, bfDefault));
	d->insert(new TButton(TRect(26, 9, 36, 11), "Cancel", cmCancel, bfNormal));

	d->selectNext(False);
	return d;
}

TDialog *createReplaceDialog()
{
	TDialog *d = new TDialog(TRect(0, 0, 40, 16), "Replace");

	d->options |= ofCentered;

	TInputLine *control = new TInputLine(TRect(3, 3, 34, 4), 80);
	d->insert(control);
	d->insert(new TLabel(TRect(2, 2, 15, 3), "~T~ext to find", control));
	d->insert(new THistory(TRect(34, 3, 37, 4), control, 10));

	control = new TInputLine(TRect(3, 6, 34, 7), 80);
	d->insert(control);
	d->insert(new TLabel(TRect(2, 5, 12, 6), "~N~ew text", control));
	d->insert(new THistory(TRect(34, 6, 37, 7), control, 11));

	d->insert(new TCheckBoxes(TRect(3, 8, 37, 12),
							  new TSItem("~C~ase sensitive",
										 new TSItem("~W~hole words only",
													new TSItem("~P~rompt on replace",
															   new TSItem("~R~eplace all", 0))))));

	d->insert(new TButton(TRect(17, 13, 27, 15), "O~K~", cmOK, bfDefault));
	d->insert(new TButton(TRect(28, 13, 38, 15), "Cancel", cmCancel, bfNormal));

	d->selectNext(False);

	return d;
}

2.6 tvedit3.cpp

cpp 复制代码
#include "tvedit.h"
TMenuBar *TEditorApp::initMenuBar(TRect r)
{
	TSubMenu &sub1 = *new TSubMenu("~F~ile", kbAltF)
					 + *new TMenuItem("~O~pen", cmOpen, kbF3, hcNoContext, "F3")
					 + *new TMenuItem("~N~ew", cmNew, kbCtrlN, hcNoContext, "Ctrl-N")
					 + *new TMenuItem("~S~ave", cmSave, kbF2, hcNoContext, "F2")
					 + *new TMenuItem("S~a~ve as...", cmSaveAs, kbNoKey) + newLine()
					 + *new TMenuItem("~C~hange dir...", cmChangeDrct, kbNoKey)
					 + *new TMenuItem("~D~OS shell", cmDosShell, kbNoKey)
					 + *new TMenuItem("E~x~it", cmQuit, kbCtrlQ, hcNoContext, "Ctrl-Q");

	TSubMenu &sub2 = *new TSubMenu("~E~dit", kbAltE)
					 + *new TMenuItem("~U~ndo", cmUndo, kbCtrlU, hcNoContext, "Ctrl-U") + newLine()
					 + *new TMenuItem("Cu~t~", cmCut, kbShiftDel, hcNoContext, "Shift-Del")
					 + *new TMenuItem("~C~opy", cmCopy, kbCtrlIns, hcNoContext, "Ctrl-Ins")
					 + *new TMenuItem("~P~aste", cmPaste, kbShiftIns, hcNoContext, "Shift-Ins")
					 + newLine()
					 + *new TMenuItem("~C~lear", cmClear, kbCtrlDel, hcNoContext, "Ctrl-Del");

	TSubMenu &sub3 = *new TSubMenu("~S~earch", kbAltS)
					 + *new TMenuItem("~F~ind...", cmFind, kbNoKey)
					 + *new TMenuItem("~R~eplace...", cmReplace, kbNoKey)
					 + *new TMenuItem("~S~earch again", cmSearchAgain, kbNoKey);

	TSubMenu &sub4 = *new TSubMenu("~W~indows", kbAltW)
					 + *new TMenuItem("~S~ize/move", cmResize, kbCtrlF5, hcNoContext, "Ctrl-F5")
					 + *new TMenuItem("~Z~oom", cmZoom, kbF5, hcNoContext, "F5")
					 + *new TMenuItem("~T~ile", cmTile, kbNoKey)
					 + *new TMenuItem("C~a~scade", cmCascade, kbNoKey)
					 + *new TMenuItem("~N~ext", cmNext, kbF6, hcNoContext, "F6")
					 + *new TMenuItem("~P~revious", cmPrev, kbShiftF6, hcNoContext, "Shift-F6")
					 + *new TMenuItem("~C~lose", cmClose, kbCtrlW, hcNoContext, "Ctrl+W");

	r.b.y = r.a.y + 1;
	return new TMenuBar(r, sub1 + sub2 + sub3 + sub4);
}

TStatusLine *TEditorApp::initStatusLine(TRect r)
{
	r.a.y = r.b.y - 1;
	return new TStatusLine(r,
						   *new TStatusDef(0, 0xFFFF) + *new TStatusItem(0, kbAltX, cmQuit)
							   + *new TStatusItem("~F2~ Save", kbF2, cmSave)
							   + *new TStatusItem("~F3~ Open", kbF3, cmOpen)
							   + *new TStatusItem("~Ctrl-W~ Close", kbAltF3, cmClose)
							   + *new TStatusItem("~F5~ Zoom", kbF5, cmZoom)
							   + *new TStatusItem("~F6~ Next", kbF6, cmNext)
							   + *new TStatusItem("~F10~ Menu", kbF10, cmMenu)
							   + *new TStatusItem(0, kbShiftDel, cmCut)
							   + *new TStatusItem(0, kbCtrlIns, cmCopy)
							   + *new TStatusItem(0, kbShiftIns, cmPaste)
							   + *new TStatusItem(0, kbCtrlF5, cmResize));
}

void TEditorApp::outOfMemory()
{
	messageBox("Not enough memory for this operation.", mfError | mfOKButton);
}

typedef char *_charPtr;
typedef TPoint *PPoint;

//#pragma warn - rvl

ushort doEditDialog(int dialog, ...)
{
	va_list arg;

	char buf[256] = {0};
	ostrstream os(buf, sizeof(buf) - 1);
	switch (dialog)
	{
	case edOutOfMemory:
		return messageBox("Not enough memory for this operation", mfError | mfOKButton);
	case edReadError:
	{
		va_start(arg, dialog);
		os << "Error reading file " << va_arg(arg, _charPtr) << "." << ends;
		va_end(arg);
		return messageBox(buf, mfError | mfOKButton);
	}
	case edWriteError:
	{
		va_start(arg, dialog);
		os << "Error writing file " << va_arg(arg, _charPtr) << "." << ends;
		va_end(arg);
		return messageBox(buf, mfError | mfOKButton);
	}
	case edCreateError:
	{
		va_start(arg, dialog);
		os << "Error creating file " << va_arg(arg, _charPtr) << "." << ends;
		va_end(arg);
		return messageBox(buf, mfError | mfOKButton);
	}
	case edSaveModify:
	{
		va_start(arg, dialog);
		os << va_arg(arg, _charPtr) << " has been modified. Save?" << ends;
		va_end(arg);
		return messageBox(buf, mfInformation | mfYesNoCancel);
	}
	case edSaveUntitled:
		return messageBox("Save untitled file?", mfInformation | mfYesNoCancel);
	case edSaveAs:
	{
		va_start(arg, dialog);
		return execDialog(new TFileDialog("*.*", "Save file as", "~N~ame", fdOKButton, 101),
						  va_arg(arg, _charPtr));
	}

	case edFind:
	{
		va_start(arg, dialog);
		return execDialog(createFindDialog(), va_arg(arg, _charPtr));
	}

	case edSearchFailed:
		return messageBox("Search string not found.", mfError | mfOKButton);
	case edReplace:
	{
		va_start(arg, dialog);
		return execDialog(createReplaceDialog(), va_arg(arg, _charPtr));
	}

	case edReplacePrompt:
		//  Avoid placing the dialog on the same line as the cursor
		TRect r(0, 1, 40, 8);
		r.move((TProgram::deskTop->size.x - r.b.x) / 2, 0);
		TPoint t = TProgram::deskTop->makeGlobal(r.b);
		t.y++;
		va_start(arg, dialog);
		TPoint *pt = va_arg(arg, PPoint);
		if (pt->y <= t.y)
			r.move(0, TProgram::deskTop->size.y - r.b.y - 2);
		va_end(arg);
		return messageBoxRect(r, "Replace this occurence?", mfYesNoCancel | mfInformation);
	}
	return cmCancel;
}

//#pragma warn.rvl

3. 编译运行

编译运行:

哇!Qt 6.9 也有字符界面啦!

4. 后记

Turbo Vision 是一个古老的库,但是由于已经被其作者进行了现代化的改造,使得在现代编译器上也运行的很好。有了它,后面控制台程序也能实现比较复杂的界面,且同时使用最新的Qt版本的各种特性。

上述代码在 msys2 ucrt64 Qt 6.9.1 下编译通过。

相关推荐
Jay_51513 分钟前
C++多态与虚函数详解:从入门到精通
开发语言·c++
路来了15 分钟前
Python小工具之PDF合并
开发语言·windows·python
追风赶月、43 分钟前
【QT】事件(鼠标、按键、定时器、窗口)
qt
xiaolang_8616_wjl1 小时前
c++文字游戏_闯关打怪
开发语言·数据结构·c++·算法·c++20
WJ.Polar1 小时前
Python数据容器-list和tuple
开发语言·python
FrostedLotus·霜莲2 小时前
C++主流编辑器特点比较
开发语言·c++·编辑器
超级码.里奥.农2 小时前
零基础 “入坑” Java--- 七、数组(二)
java·开发语言
KENYCHEN奉孝2 小时前
Rust征服字节跳动:高并发服务器实战
服务器·开发语言·rust
挺菜的2 小时前
【算法刷题记录(简单题)002】字符串字符匹配(java代码实现)
java·开发语言·算法
妮妮喔妮3 小时前
【无标题】
开发语言·前端·javascript