用 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 环境下来做, 并在Linux环境 (Manjaro)下也做一遍。。
1. 下载并编译 Turbo Vision
git clone https://github.com/magiblot/tvision.gitcd tvision/cmake . -B ./build/ucrt64 -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release && cmake --build ./build/ucrt64
如果需要参考教材,可以继续克隆教材库:
cd ..
git clone https://github.com/yym36100/t3_rom.git
编译完毕后,可以直接进入文件夹运行例子。这里不再赘述。教材很厚,非常佩服以前的纸质教材:
编译好后,发现库是静态的!太棒了。
2. 从Editor例子构造Qt应用
我们从TVisition的例子入手,构造Qt应用。下面完整附加所有代码。
2.1 工程文件
Qt控制台程序,6.9.1,文件名为qtvedit.pro:
QT = core concurrent
CONFIG += c++17 cmdlineHEADERS += \editor/tvedit.h
SOURCES += \editor/tvedit1.cpp \editor/tvedit2.cpp \editor/tvedit3.cpp \main.cppTVISION_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:
#include <QCoreApplication>
#include <QtConcurrentRun>
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函数:
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:
#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
#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 specifiedopenEditor(*++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;elseswitch (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
#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
#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 - rvlushort 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 cursorTRect 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. 在 Linux 下构造
Turbo Vision 是一个古老的库,但是由于已经被其作者进行了现代化的改造,使得在现代编译器上也运行的很好。有了它,后面控制台程序也能实现比较复杂的界面,且同时使用最新的Qt版本的各种特性。上述代码在 msys2 ucrt64 Qt 6.9.1 下编译通过。下面,在Linux下简单重复上述步骤:
git clone https://github.com/magiblot/tvision.git
cd tvision/
mkdir build
cd build/
mkdir linux64
cd linux64
cmake ../..
make -j 8
Linux下,tvision依赖 curses 和 gpm库,所以工程文件稍微改一下:
QT = core concurrent
CONFIG += c++17 cmdlineHEADERS += \editor/tvedit.h
SOURCES += \editor/tvedit1.cpp \editor/tvedit2.cpp \editor/tvedit3.cpp \main.cpp#这是git clone 的文件夹路径
TVISION_PATH = /home/user/projects/tvision
#一般构建 tvision 的位置是在路径下的 build/,如果考虑多个平台,则外加平台名称。
#在windows下,我们的msys2 ucrt64环境下,构建位置是在 $$TVISION_PATH/build/ucrt64INCLUDEPATH += $$TVISION_PATH/include
#Only for this example
INCLUDEPATH += $$TVISION_PATH/include/tvision/compat/borlandLIBS += -L$$TVISION_PATH/build/linux64 -ltvision
linux: LIBS +=-lcurses -lgpm
编译完毕后,使用ssh登入,在没有X的情况下,也能运行字符窗口界面(TUI)了:
5. 关于Turbo和Borland的回忆
1996年,是面向对象编程的活跃时代。对于上了年纪的人,非常怀念老式卡带、邓丽君、毛阿敏,还有刚买了电视机就观看的电视剧《渴望》。Borland在1990年代推出的Turbo系列工具,包括 Turbo C\C++、Turbo Pascal、Turbo Debugger、Turbo ASM、 Turbo BASIC,编织了属于软件个人英雄时代的传奇。记得有一本书叫做《Borland传奇》,写的不错。
在Linux下的自绘TUI还有类似MC,VIM,Nano等环境,包括 raspi-config,但是这些对于GUI行为的模仿要么不是很贴合,风格也不是很一致,要么就是配置很复杂。 在1990年代,TUI对于构造傻瓜化的IDE非常重要。现如今,图形化已经不再是成本和性能的瓶颈,但是基于TUI的APP本身还是为一些场景提供了更多的选择。与 Turbo Vision 同风格的是微软的TUI界面,大量在 EDITOR、Quick BASIC、Programmers’ Working Bench(PWB)、Viusal BASIC for DOS下使用。