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 下编译通过。