Привет добрым людям.
При прочтении этого заголовка читатели могут подумать: зачем смешивать консольные и графические приложения – консоль в GUI-приложении не нужна. А вот и нет, смею заметить. Иногда совмещение функциональной консоли с полным набором команд и графического отображения для удобной навигации и просмотра данных может дать в итоге мощный инструмент.
И у меня есть пример.
Начав использовать быстрое key-value хранилище данных Redis для своих проектов, я обнаружил, что на данный момент нет ни одного вменяемого desktop-приложения для просмотра, редактирования и администрирования баз данных Redis. Есть только консоль от разработчиков, веб-интерфейс Redis Admin UI, который для своей работы требует .NET (что само по себе уже отпугивает) и пару Ruby-приложений, сделанных, похоже, на скорую руку, на коленке.
Хотелось бы иметь что-то удобное и быстрое, как сама база данных Redis. Поэтому я решил восполнить этот пробел и написать такой инструмент. Так как нужен быстрый – то C++, так как нужен кроссплатформенный – то Qt.

Из-за того, что все возможности базы данных не реализуешь, да они и могут появляться каждый день новые, нужно было добавить в графический интерфейс консоль. На основе какого виджета в Qt ее имитировать, и как, и хочу вам рассказать.
Для базового виджета консоли я выбрал QPlainTextEdit. Во-первых, он включает в себя расширенные возможности редактирования текста, которые нам могут понадобиться, а во-вторых, он позволяет добавлять форматирование: подсветка разных элементов цветом нам бы не помешала.
Итак, создаем потомка QPlainTextEdit.
Несмотря на то, что QPlainTextEdit – это упрощенная версия QTextEdit, он разрешает пользователю делать черезчур большое количество действий, непозволительное для приличной консоли.
Поэтому первое, что мы сделаем, — это ограничим все, что только можно. Перейдем от полного беспредела к тотальному контролю.
Для этого переопределим встроенные слоты, получающие нажатия клавиш и клики мышки:
После этих строк пользователь не сможет ни ввести символ в поле виджета, ни выделить кусок текста, ни удалить строку – полная блокировка.
Теперь пойдем от тотального запрета к разумной демократии, попутно разрешая все, что понадобится.
Первое, что сделаем – это определим строку приглашения (prompt):
И выведем строку приглашения в консоль:
Нужно, чтобы при клике мышкой нельзя было переставить курсор, но можно было сделать консоль активной:
При вводе обычных букв, цифр и других полезных символов, они должны добавляться в строку команды:
Символы можно стирать клавишей Backspace, но не все, а только до определенного момента – чтобы строка приглашения не дай бог не затерлась:
Определим реакцию виджета на ввод команды (при нажатии клавиши Enter):
При вводе команды мы вырезаем кусок текста от строки приглашения до конца текстового блока и испускаем сигнал, к которому можно будет присоединить слот:
Так же на время обработки команды приложением, устанавливаем флажок блокировки текстового поля.
Приложение – родитель виджета обработает команду и передаст консоли результат выполнения, тем самым разблокируя ее:
Хотелось бы, чтобы история всех вводимых команд сохранялась и при нажатии клавиш вверх/вниз можно было бы по ней перемещаться:
Для этого в конструкторе виджета определим общую гамму цвета для консоли – фон черный, буквы вводимой команды – зеленые:
При выводе строки приглашения делаем шрифт зеленого цвета:
А при выводе результата выполнения команды делаем шрифт белого цвета:
Также хотелось бы, чтобы когда пользователь вводит команду, скроллбар текстового поля консоли проматывался до самого низа:
В результате получилась веселая, красивая и удобная консолька. У меня это заняло всего 120 строк кода. Конечно, есть еще много вещей, которые можно было бы сделать, но основная функциональность реализована.
Исходный код проекта RedisConsole на GitHub: https://github.com/ptrofimov/RedisConsole
Там можно посмотреть класс виджета Console и скачать скомпилированный бинарник приложения для Windows, нажав кнопку «Downloads».
При прочтении этого заголовка читатели могут подумать: зачем смешивать консольные и графические приложения – консоль в GUI-приложении не нужна. А вот и нет, смею заметить. Иногда совмещение функциональной консоли с полным набором команд и графического отображения для удобной навигации и просмотра данных может дать в итоге мощный инструмент.
И у меня есть пример.
Начав использовать быстрое key-value хранилище данных Redis для своих проектов, я обнаружил, что на данный момент нет ни одного вменяемого desktop-приложения для просмотра, редактирования и администрирования баз данных Redis. Есть только консоль от разработчиков, веб-интерфейс Redis Admin UI, который для своей работы требует .NET (что само по себе уже отпугивает) и пару Ruby-приложений, сделанных, похоже, на скорую руку, на коленке.
Хотелось бы иметь что-то удобное и быстрое, как сама база данных Redis. Поэтому я решил восполнить этот пробел и написать такой инструмент. Так как нужен быстрый – то C++, так как нужен кроссплатформенный – то Qt.

Из-за того, что все возможности базы данных не реализуешь, да они и могут появляться каждый день новые, нужно было добавить в графический интерфейс консоль. На основе какого виджета в Qt ее имитировать, и как, и хочу вам рассказать.
От беспредела к тотальному контролю
Для базового виджета консоли я выбрал QPlainTextEdit. Во-первых, он включает в себя расширенные возможности редактирования текста, которые нам могут понадобиться, а во-вторых, он позволяет добавлять форматирование: подсветка разных элементов цветом нам бы не помешала.
Итак, создаем потомка QPlainTextEdit.
class Console : public QPlainTextEdit{};
Несмотря на то, что QPlainTextEdit – это упрощенная версия QTextEdit, он разрешает пользователю делать черезчур большое количество действий, непозволительное для приличной консоли.
Поэтому первое, что мы сделаем, — это ограничим все, что только можно. Перейдем от полного беспредела к тотальному контролю.
Для этого переопределим встроенные слоты, получающие нажатия клавиш и клики мышки:
void Console::keyPressEvent(QKeyEvent *){} void Console::mousePressEvent(QMouseEvent *){} void Console::mouseDoubleClickEvent(QMouseEvent *){} void Console::contextMenuEvent(QContextMenuEvent *){}
После этих строк пользователь не сможет ни ввести символ в поле виджета, ни выделить кусок текста, ни удалить строку – полная блокировка.
Этап либерализации
Теперь пойдем от тотального запрета к разумной демократии, попутно разрешая все, что понадобится.
Первое, что сделаем – это определим строку приглашения (prompt):
// class definition QString prompt; // contructor prompt = "redis> ";
И выведем строку приглашения в консоль:
// constructor insertPrompt(false); // source void Console::insertPrompt(bool insertNewBlock) { if(insertNewBlock) textCursor().insertBlock(); textCursor().insertText(prompt); }
Нужно, чтобы при клике мышкой нельзя было переставить курсор, но можно было сделать консоль активной:
void Console::mousePressEvent(QMouseEvent *) { setFocus(); }
При вводе обычных букв, цифр и других полезных символов, они должны добавляться в строку команды:
void Console::keyPressEvent(QKeyEvent *event) { // … if(event->key() >= 0x20 && event->key() <= 0x7e && (event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::ShiftModifier)) QPlainTextEdit::keyPressEvent(event); // … }
Символы можно стирать клавишей Backspace, но не все, а только до определенного момента – чтобы строка приглашения не дай бог не затерлась:
void Console::keyPressEvent(QKeyEvent *event) { // … if(event->key() == Qt::Key_Backspace && event->modifiers() == Qt::NoModifier && textCursor().positionInBlock() > prompt.length()) QPlainTextEdit::keyPressEvent(event); // … }
Определим реакцию виджета на ввод команды (при нажатии клавиши Enter):
void Console::keyPressEvent(QKeyEvent *event) { // … if(event->key() == Qt::Key_Return && event->modifiers() == Qt::NoModifier) onEnter(); // … }
При вводе команды мы вырезаем кусок текста от строки приглашения до конца текстового блока и испускаем сигнал, к которому можно будет присоединить слот:
void Console::onEnter() { if(textCursor().positionInBlock() == prompt.length()) { insertPrompt(); return; } QString cmd = textCursor().block().text().mid(prompt.length()); emit onCommand(cmd); }
Так же на время обработки команды приложением, устанавливаем флажок блокировки текстового поля.
void Console::onEnter() { // … isLocked = true; } void Console::keyPressEvent(QKeyEvent *event) { if(isLocked) return; // … }
Приложение – родитель виджета обработает команду и передаст консоли результат выполнения, тем самым разблокируя ее:
void Console::output(QString s) { textCursor().insertBlock(); textCursor().insertText(s); insertPrompt(); isLocked = false; }
История команд
Хотелось бы, чтобы история всех вводимых команд сохранялась и при нажатии клавиш вверх/вниз можно было бы по ней перемещаться:
// 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++; }
Делаем красиво: раскраска консоли
Для этого в конструкторе виджета определим общую гамму цвета для консоли – фон черный, буквы вводимой команды – зеленые:
QPalette p = palette(); p.setColor(QPalette::Base, Qt::black); p.setColor(QPalette::Text, Qt::green); setPalette(p);
При выводе строки приглашения делаем шрифт зеленого цвета:
void Console::insertPrompt(bool insertNewBlock) { // … QTextCharFormat format; format.setForeground(Qt::green); textCursor().setBlockCharFormat(format); // … }
А при выводе результата выполнения команды делаем шрифт белого цвета:
void Console::output(QString s) { // … QTextCharFormat format; format.setForeground(Qt::white); textCursor().setBlockCharFormat(format); // … }
Все вниз!
Также хотелось бы, чтобы когда пользователь вводит команду, скроллбар текстового поля консоли проматывался до самого низа:
void Console::insertPrompt(bool insertNewBlock) { // … scrollDown(); } void Console::scrollDown() { QScrollBar *vbar = verticalScrollBar(); vbar->setValue(vbar->maximum()); }
Результат
В результате получилась веселая, красивая и удобная консолька. У меня это заняло всего 120 строк кода. Конечно, есть еще много вещей, которые можно было бы сделать, но основная функциональность реализована.
Ссылки
Исходный код проекта RedisConsole на GitHub: https://github.com/ptrofimov/RedisConsole
Там можно посмотреть класс виджета Console и скачать скомпилированный бинарник приложения для Windows, нажав кнопку «Downloads».