Привет добрым людям.
При прочтении этого заголовка читатели могут подумать: зачем смешивать консольные и графические приложения – консоль в 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».