📜 ⬆️ ⬇️

Qt. Creating a console widget for a graphics application

Hello good people.
When reading this title, readers might think: why mix console and graphics applications - the console is not needed in a GUI application. Well, no, I dare say. Sometimes combining a functional console with a full set of commands and a graphical display for easy navigation and viewing of data can result in a powerful tool.
And I have an example.
Having started using the fast key-value data store Redis for my projects, I discovered that at the moment there are no sane desktop applications for viewing, editing and administering Redis databases. There is only a console from the developers, the Redis Admin UI web interface, which requires .NET for its work (which in itself is scaring off) and a pair of Ruby applications, which seem to be in a hurry, on the knee.
I would like to have something convenient and fast, like the Redis database itself. Therefore, I decided to fill this gap and write such a tool. Since you need a fast one, then C ++, since you need a cross-platform one, then Qt.

RedisConsole

Due to the fact that you do not implement all the capabilities of the database, and they may appear new every day, you had to add a console to the graphical interface. Based on which widget in Qt imitate it, and how, and I want to tell you.
')

From mayhem to total control



For the base console widget, I selected QPlainTextEdit. Firstly, it includes advanced text editing features that we may need, and secondly, it allows you to add formatting: highlighting different elements with color would not hurt us.

So, create a descendant of QPlainTextEdit.

class Console : public QPlainTextEdit{}; 


Despite the fact that QPlainTextEdit is a simplified version of QTextEdit, it allows the user to do too many actions that are not allowed for a decent console.

Therefore, the first thing we will do is limit everything that is possible. Let's move from complete chaos to total control.

To do this, override the built-in slots that receive keystrokes and mouse clicks:

 void Console::keyPressEvent(QKeyEvent *){} void Console::mousePressEvent(QMouseEvent *){} void Console::mouseDoubleClickEvent(QMouseEvent *){} void Console::contextMenuEvent(QContextMenuEvent *){} 


After these lines, the user will not be able to either enter a character in the widget field, neither select a piece of text, nor delete a line — a complete lock.

Stage of liberalization



Now let's go from total prohibition to rational democracy, at the same time allowing everything that is needed.

The first thing we will do is define the prompt string (prompt):

 // class definition QString prompt; // contructor prompt = "redis> "; 


And we will output the prompt to the console:

 // constructor insertPrompt(false); // source void Console::insertPrompt(bool insertNewBlock) { if(insertNewBlock) textCursor().insertBlock(); textCursor().insertText(prompt); } 


It is necessary that when you click the mouse you can not move the cursor, but you can make the console active:

 void Console::mousePressEvent(QMouseEvent *) { setFocus(); } 


When typing ordinary letters, numbers and other useful characters, they should be added to the command line:

 void Console::keyPressEvent(QKeyEvent *event) { // … if(event->key() >= 0x20 && event->key() <= 0x7e && (event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::ShiftModifier)) QPlainTextEdit::keyPressEvent(event); // … } 


You can erase the characters with the Backspace key, but not all, but only up to a certain point - so that God forbid the line of the invitation do not get stuck:

 void Console::keyPressEvent(QKeyEvent *event) { // … if(event->key() == Qt::Key_Backspace && event->modifiers() == Qt::NoModifier && textCursor().positionInBlock() > prompt.length()) QPlainTextEdit::keyPressEvent(event); // … } 


Define the reaction of the widget to the command input (by pressing the Enter key):

 void Console::keyPressEvent(QKeyEvent *event) { // … if(event->key() == Qt::Key_Return && event->modifiers() == Qt::NoModifier) onEnter(); // … } 


When entering a command, we cut a piece of text from the invitation line to the end of the text block and emit a signal to which you can attach a slot:

 void Console::onEnter() { if(textCursor().positionInBlock() == prompt.length()) { insertPrompt(); return; } QString cmd = textCursor().block().text().mid(prompt.length()); emit onCommand(cmd); } 


Also at the time of processing the command by the application, set the text box lock checkbox.

 void Console::onEnter() { // … isLocked = true; } void Console::keyPressEvent(QKeyEvent *event) { if(isLocked) return; // … } 


The application - the parent of the widget will process the command and transmit the result of execution to the console, thereby unblocking it:

 void Console::output(QString s) { textCursor().insertBlock(); textCursor().insertText(s); insertPrompt(); isLocked = false; } 


Command history



I would like the history of all the commands entered to be saved and when you press the up / down keys, you can navigate through it:

 // class definition QStringList *history; int historyPos; // source void Console::keyPressEvent(QKeyEvent *event) { // … if(event->key() == Qt::Key_Up && event->modifiers() == Qt::NoModifier) historyBack(); if(event->key() == Qt::Key_Down && event->modifiers() == Qt::NoModifier) historyForward(); } void Console::onEnter() { // … historyAdd(cmd); // … } void Console::historyAdd(QString cmd) { history->append(cmd); historyPos = history->length(); } void Console::historyBack() { if(!historyPos) return; QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::StartOfBlock); cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); cursor.removeSelectedText(); cursor.insertText(prompt + history->at(historyPos-1)); setTextCursor(cursor); historyPos--; } void Console::historyForward() { if(historyPos == history->length()) return; QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::StartOfBlock); cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); cursor.removeSelectedText(); if(historyPos == history->length() - 1) cursor.insertText(prompt); else cursor.insertText(prompt + history->at(historyPos + 1)); setTextCursor(cursor); historyPos++; } 


Making it beautiful: coloring the console



To do this, in the widget's constructor, we define the general color gamut for the console — the background is black, the letters of the command being entered are green:

 QPalette p = palette(); p.setColor(QPalette::Base, Qt::black); p.setColor(QPalette::Text, Qt::green); setPalette(p); 


When displaying the prompt, we make the font green:

 void Console::insertPrompt(bool insertNewBlock) { // … QTextCharFormat format; format.setForeground(Qt::green); textCursor().setBlockCharFormat(format); // … } 


And when displaying the result of the command, we make the font in white:

 void Console::output(QString s) { // … QTextCharFormat format; format.setForeground(Qt::white); textCursor().setBlockCharFormat(format); // … } 


All down!



I would also like that when the user enters the command, the scrollbar of the console text field is scrolled all the way down:

 void Console::insertPrompt(bool insertNewBlock) { // … scrollDown(); } void Console::scrollDown() { QScrollBar *vbar = verticalScrollBar(); vbar->setValue(vbar->maximum()); } 


Result



The result was a fun, beautiful and comfortable console. It took me only 120 lines of code. Of course, there are still many things that could be done, but the main functionality is implemented.

Links



The source code of the RedisConsole project on GitHub: https://github.com/ptrofimov/RedisConsole

There you can view the class of the Console widget and download the compiled application binary for Windows by clicking the “Downloads” button.

thank

Source: https://habr.com/ru/post/122831/


All Articles