БлогNot. QT: действительно простое приложение MVC

QT: действительно простое приложение MVC

Концепция MVC, показанная на готовых QT'шных классах, отличающихся громоздкостью, выглядит не всегда понятной :) Меж тем, её можно продемонстрировать на чём-то гораздо более стандартном, например, на матрице целых чисел, редактируемой прямо в консоли.

Попробуем всё сделать по правилам. Создадим проект QT, который будет компилироваться, как консольное приложение, то есть, файл проекта .pro имеет вид

QT       += core
QT       -= gui
TARGET = Console1
CONFIG   += console
CONFIG   -= app_bundle
TEMPLATE = app

Файлы с моделью, видом и контроллером будем добавлять в проект как обычные заголовочные файлы с расширением .h

Сначала придётся применить немного абстрактного мышления.

Класс AbstractMatrixModel (файл abstract_matrix_model.h) будет содержать только виртуальные методы, которые нужны любой конкретной модели нашей предметной области. Что должна уметь модель матрицы? Получать элемент из нужной строки и столбца, записывать этот элемент обратно, узнавать количество строк и количество столбцов матрицы. Так и скажем:

#ifndef ABSTRACT_MATRIX_MODEL_H_INCLUDED
#define ABSTRACT_MATRIX_MODEL_H_INCLUDED

#include <cstddef>

class AbstractMatrixModel {
public:
 virtual int  getData(size_t row, size_t column) const = 0;
 virtual void setData(size_t row, size_t column, int value) = 0;
 virtual size_t columns() const = 0;
 virtual size_t rows() const = 0;
};

#endif // ABSTRACT_MATRIX_MODEL_H_INCLUDED

С видом всё ещё проще (файл abstract_view.h). Достаточно иметь методы для вывод матрицы в консоль и для обработки событий от клавиатуры (мы собираемся редактировать в консоли нашу матрицу). Вот виртуальные прототипы будущих методов вида:

#ifndef ABSTRACT_VIEW_H_INCLUDED
#define ABSTRACT_VIEW_H_INCLUDED

class AbstractView {
public:
 virtual void event(int key) = 0;
 virtual void show() = 0;
};

#endif // ABSTRACT_VIEW_H_INCLUDED

Теперь моделируем матрицу (файл matrix_model.h). Как и положено, данные будут храниться в приватной секции класса (указатель int * matrix_), для простоты представим матрицу как вектор, всё равно все действия с её элементами инкапсулированы в методы getData и setData. Вот какой класс вышел:

#ifndef MATRIX_MODEL_H_INCLUDED
#define MATRIX_MODEL_H_INCLUDED

#include "abstract_matrix_model.h"

class MatrixModel : public AbstractMatrixModel {

private:
 int * matrix_;
 size_t rows_, cols_;

public:
 
 MatrixModel(size_t rows, size_t cols)
  : matrix_(new int[rows * cols]()), rows_(rows), cols_(cols) {}
 
 ~MatrixModel() {
  delete[] matrix_;
 }

 int getData(size_t row, size_t col) const {
  return matrix_[col * rows_ + row];
 }

 void setData(size_t row, size_t col, int value) {
  matrix_[col * rows_ + row] = value;
 }

 size_t columns() const {
  return cols_;
 }
 
 size_t rows() const {
  return rows_;
 }
};

#endif // MATRIX_MODEL_H_INCLUDED

Прежде, чем писать вид, позаботимся о вспомогательных сущностях.

Класс ConsoleCursor (файл console_cursor.h) просто следит за виртуальными координатами курсора, сохранёнными в приватных целых переменных row_, col_. Сколько всего будет доступно строк и столбцов, мы пока не знаем, поэтому методы позиционирования курсора будут получать нужную информацию через параметры rows, columns.

Конечно, ему придётся также заботиться о позиционировании курсора, этим займётся статический метод setXY, используя возможности Windows API. Вот полный текст класса:

#ifndef CONSOLE_CURSOR_H_INCLUDED
#define CONSOLE_CURSOR_H_INCLUDED

#include <windows.h>

class ConsoleCursor {

private:
 int row_, col_;

public:
 ConsoleCursor(int row = 0, int col = 0):row_(row),col_(col) {}

 void up(size_t rows) {
  row_ = row_ == 0 ? rows - 1 : row_ - 1;
 }
 void down(size_t rows) {
  row_ = size_t(row_ + 1) == rows ? 0 : row_ + 1;
 }
 void left(size_t columns) {
  col_ = col_ == 0 ? columns - 1 : col_ - 1;
 }
 void right(size_t columns) {
  col_ = size_t(col_ + 1) == columns ? 0 : col_ + 1;
 }

 int column() const { return col_; }
 int row() const { return row_; }

 void moveCursor(int x, int y) {
  setXY(x + col_ * 3, y + row_);
 }

 static void setXY(short x, short y) {
  COORD pos = { x, y };
  HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);
  SetConsoleCursorPosition(output, pos);
 }
};

#endif // CONSOLE_CURSOR_H_INCLUDED

Маленькое перечисление ConsoleKeys, которое мы лишь для порядка сохранили в отдельном файле console_keys.h, просто будет содержать коды клавиш, обрабатываемых нашим представлением:

#ifndef KEYS_H_INCLUDED
#define KEYS_H_INCLUDED

enum ConsoleKeys {
 KEY_ESC     = 27,
 ARROW_UP    = 256 + 72,
 ARROW_DOWN  = 256 + 80,
 ARROW_LEFT  = 256 + 75,
 ARROW_RIGHT = 256 + 77,
 KEY_ENTER   = 13
};

#endif // KEYS_H_INCLUDED

Теперь, пожалуй, время писать само представление, или вид, как его часто называют (файл console_matrix_view.h). Метод show, как ему и положено, будет показывать матрицу, а event обеспечит "навигацию", сводящуюся к перемещению курсора по числам. Поможет ему в этом служебный метод input, отвечающий за ввод данных с клавиатуры. Прежде, чем что-то делать, методы смотрят, подключена ли модель (указатель model_). А поставит его, как и положено в MVC, метод setModel. Вот полный код класса:

#ifndef CONSOLE_MATRIX_VIEW_H_INCLUDED
#define CONSOLE_MATRIX_VIEW_H_INCLUDED

#include "abstract_view.h"
#include "abstract_matrix_model.h"
#include "console_cursor.h"
#include "console_keys.h"

#include <cstdio>

class ConsoleMatrixView : public AbstractView {
private:
 int x_, y_;
 ConsoleCursor cursor_;
 AbstractMatrixModel * model_;

public:
 ConsoleMatrixView(int x, int y) : x_(x),y_(y),cursor_() { }

 /*virtual*/ void show() {
  if(model_) {
   size_t rows = model_->rows();
   size_t columns = model_->columns();
   for (size_t i = 0; i < rows; ++i) {
    for (size_t j = 0; j < columns; ++j) {
     ConsoleCursor(i, j).moveCursor(x_, y_);
     printf("%02d ", model_->getData(i, j));
    }
   }
   cursor_.moveCursor(x_, y_);
  }
 }

 size_t columns() const {
  return model_ ? model_->columns() : 0;
 }

 size_t rows() const {
  return model_ ? model_->rows() : 0;
 }

 void setModel(AbstractMatrixModel * model) {
  model_ = model;
 }

private:
 void input() {
  char buf[3] = {'0','0','0'};
  if(fgets(buf, sizeof(buf), stdin) != 0) {
   int val;
   if(sscanf(buf, "%d", &val) == 1) {
    model_->setData(cursor_.row(), cursor_.column(), val);
   }
   show();
   fflush(stdin);
  }
 }

/*virtual*/ void event(int key) {
 if (model_) {
  switch(key) {
   case ARROW_UP:
    cursor_.up(rows());
    cursor_.moveCursor(x_, y_);
   break;
   case ARROW_DOWN:
    cursor_.down(rows());
    cursor_.moveCursor(x_, y_);
   break;
   case ARROW_LEFT:
    cursor_.left(columns());
    cursor_.moveCursor(x_, y_);
   break;
   case ARROW_RIGHT:
    cursor_.right(columns());
    cursor_.moveCursor(x_, y_);
   break;
   case KEY_ENTER:
    input();
    break;
   }
  }
 }

};
#endif // CONSOLE_MATRIX_VIEW_H_INCLUDED

Ах да, "триаде" же ещё контроллер нужен :) Кстати, когда модель спроектирована плохо, у неё всегда получаются огромные и непонятные контроллеры, даже есть специальный термин FSUC - "толстые тупые уродливые контроллеры" (Fat Stupid Ugly Controllers). Но наш контроллер будет тонким и умным, ведь ему хватит методов exec (запуститься) и setMainView (установить представление, с которым работаем). Вот полный код файла controller.h:

#ifndef CONTROLLER_H_INCLUDED
#define CONTROLLER_H_INCLUDED

#include <cstdio>
#include <conio.h>

#include "abstract_view.h"
#include "console_keys.h"

class Controller {
private:
 AbstractView * view_;
public:
 void exec() {
  if (view_) {
   view_->show();
   int ch;
   while ((ch = getCode()) != KEY_ESC) {
    view_->event(ch);
   }
  }
 }

 void setMainView(AbstractView * view) {
  view_ = view;
 }

private:
 static int getCode() {
  int ch = getch();
  if (ch == 0 || ch == 224) {
   ch = 256 + getch();
  }
  return ch;
 }
};
#endif // CONTROLLER_H_INCLUDED

Теперь добавим в проект файл main.cpp, который всё это запустит "по правилам MVC":

#include "console_matrix_view.h"
#include "matrix_model.h"
#include "controller.h"

int main() {
 Controller control;
 MatrixModel model(10, 15); //размерности
 ConsoleMatrixView view (1, 1); //позиция вывода
 view.setModel(&model);
 control.setMainView(&view);
 control.exec();
}

Войти в редактирование элемента можно по Enter, ходим стрелочками, для выхода, как видно из листинга, нажмите Esc, потом Enter. Всё готово и работает, правда, контролем правильности ввода "на лету" мы здесь не озадачивались, но проект ведь можно и развить :)

Ну и помните главное - MVC не так страшен, как его малюют, а наоборот удобен, и показанное решение - отнюдь не единственное или лучшее, наверняка можно сделать и короче, и лучше.

скриншот приложения в работе
скриншот приложения в работе

 Архив .zip с папкой этого проекта QT5 (4 Кб)

12.04.2016, 16:48 [6452 просмотра]


теги: программирование c++ qt

К этой статье пока нет комментариев, Ваш будет первым