Приставка-на основе структуры таблицы


Лично я часто предпочитаю консольные приложения над приложений с графическим интерфейсом. Однако, иногда она достаточно сложна, чтобы отобразить все хорошо выровнены. Поэтому я решил создать консольный стол, который позволяет отображать данные организованы в категории. Вы можете добавлять, удалять и обновлять записи и выбрать между различных конструкций. Это пример, как это выглядит.

enter image description here enter image description here

(Некоторые символы могут неправильно отображаться в консоли Windows. В терминале Linux все символы печатаются без проблем.)

Я хотел бы получить некоторую обратную связь, что я могу улучшить в этом классе, и если есть какие-то вредные привычки.

ConsoleTable.ч

#ifndef CONSOLETABLE_CONSOLETABLE_H
#define CONSOLETABLE_CONSOLETABLE_H

#include <string>
#include <vector>
#include <iostream>
#include "ConsoleTableRow.h"
#include "ConsoleTableUtils.h"
#include <sstream>

enum TableStyle {
    BASIC,
    LINED,
    DOUBLE_LINE,
};

enum HorizontalSeperator{
    SEPERATOR_TOP,
    SEPERATOR_MIDDLE,
    SEPERATOR_BOTTOM
};

class ConsoleTable {
public:

    ConsoleTable(TableStyle style);

    void setPadding(unsigned int width);

    void addColumn(std::string name);

    void addRow(ConsoleTableRow *item);

    bool removeRow(int index);

    bool editRow(std::string data, int row, int col);

    void printTable();

private:

    unsigned int padding = 1;

    std::vector<std::string> columns;
    std::vector<ConsoleTableRow *> entries;
    ConsoleTableUtils* utils;

    // Table Style variables
    std::string style_line_horizontal;
    std::string style_line_vertical;
    std::string style_line_cross;
    std::string style_t_intersect_right;
    std::string style_t_intersect_left;
    std::string style_t_intersect_top;
    std::string style_t_intersect_bottom;
    std::string style_edge_topleft;
    std::string style_edge_topright;
    std::string style_edge_buttomleft;
    std::string style_edge_buttomright;

    void printHorizontalSeperator(const std::vector<int> &maxWidths, HorizontalSeperator seperator) const;

    void setTableStyle(TableStyle style);

};


#endif //CONSOLETABLE_CONSOLETABLE_H

ConsoleTable.cpp

#include "ConsoleTable.h"


ConsoleTable::ConsoleTable(TableStyle style) {
    setTableStyle(style);
    this->utils = new ConsoleTableUtils();
}

void ConsoleTable::addColumn(std::string name) {
    this->columns.push_back(name);
}

void ConsoleTable::printTable() {

    // Calculate column maxima
    std::vector<int> maxWidths(this->columns.size());
    for (int row = 0; row < this->entries.size(); row++) {
        for (int col = 0; col < this->columns.size(); col++) {
            std::string cellText = this->entries[row]->getEntry()[col];
            if (this->columns[col].length() > maxWidths[col])
                maxWidths[col] = this->columns[col].length();
            if (maxWidths[col] < cellText.length()) {
                maxWidths[col] = cellText.length();
            }
        }
    }

    printHorizontalSeperator(maxWidths, SEPERATOR_TOP);

    // Print column values
    for (int col = 0; col < this->columns.size(); col++) {
        std::string cellText = this->columns[col];
        int len = cellText.length();
        std::string paddedText = cellText + std::string(maxWidths[col] - len, ' ');
        std::cout << this->style_line_vertical << std::string(this->padding, ' ') << paddedText
                  << std::string(this->padding, ' ');
        std::cout << (col == this->columns.size() - 1 ? this->style_line_vertical + "\n" : "");
    }

    printHorizontalSeperator(maxWidths, SEPERATOR_MIDDLE);

    // Print cell values
    for (int row = 0; row < this->entries.size(); row++) {
        for (int col = 0; col < this->columns.size(); col++) {
            std::string cellText = this->entries[row]->getEntry()[col];
            std::string paddedText = cellText + std::string(maxWidths[col] - cellText.length(), ' ');
            std::cout << this->style_line_vertical << std::string(this->padding, ' ') << paddedText
                      << std::string(this->padding, ' ');
        }
        std::cout << this->style_line_vertical << std::endl;
        if (row == this->entries.size() - 1)
            printHorizontalSeperator(maxWidths, SEPERATOR_BOTTOM);
        else
            printHorizontalSeperator(maxWidths, SEPERATOR_MIDDLE);
    }
}

void ConsoleTable::printHorizontalSeperator(const std::vector<int> &maxWidths, HorizontalSeperator seperator) const {
    for (int col = 0; col < columns.size(); ++col) {

        switch (seperator) {
            case SEPERATOR_TOP: {
                std::cout << (col == 0 ? this->style_edge_topleft : "");
                std::cout << utils->repeatString(this->style_line_horizontal, this->padding);
                std::cout << utils->repeatString(this->style_line_horizontal, maxWidths[col]);
                std::cout << utils->repeatString(this->style_line_horizontal, this->padding);
                std::cout << (col != columns.size() - 1 ? this->style_t_intersect_top : this->style_edge_topright);
                std::cout << (col == columns.size() - 1 ? "\n" : "");
                break;
            }
            case SEPERATOR_MIDDLE: {
                std::cout << (col == 0 ? this->style_t_intersect_left : "");
                std::cout << utils->repeatString(this->style_line_horizontal, this->padding);
                std::cout << utils->repeatString(this->style_line_horizontal, maxWidths[col]);
                std::cout << utils->repeatString(this->style_line_horizontal, this->padding);
                std::cout << (col != columns.size() - 1 ? this->style_line_cross : this->style_t_intersect_right);
                std::cout << (col == columns.size() - 1 ? "\n" : "");
                break;
            }
            case SEPERATOR_BOTTOM: {
                std::cout << (col == 0 ? this->style_edge_buttomleft : "");
                std::cout << utils->repeatString(this->style_line_horizontal, this->padding);
                std::cout << utils->repeatString(this->style_line_horizontal, maxWidths[col]);
                std::cout << utils->repeatString(this->style_line_horizontal, this->padding);
                std::cout
                        << (col != columns.size() - 1 ? this->style_t_intersect_bottom : this->style_edge_buttomright);
                std::cout << (col == columns.size() - 1 ? "\n" : "");
                break;
            }
        }
    }
}

void ConsoleTable::addRow(ConsoleTableRow *item) {
    this->entries.push_back(item);
}

bool ConsoleTable::removeRow(int index) {
    if (index > this->entries.size())
        return false;
    this->entries.erase(this->entries.begin() + index);
    return true;
}

bool ConsoleTable::editRow(std::string data, int row, int col) {
    if(row > this->entries.size())
        return false;

    if(col > this->columns.size())
        return false;

    auto entry = this->entries[row];
    entry->editEntry(data, col);
    return true;
}

void ConsoleTable::setPadding(unsigned int width) {
    this->padding = width;
}

void ConsoleTable::setTableStyle(TableStyle style) {
    switch (style) {
        case BASIC: {
            this->style_line_horizontal = "-";
            this->style_line_vertical = "|";
            this->style_line_cross = "+";

            this->style_t_intersect_right = "+";
            this->style_t_intersect_left = "+";
            this->style_t_intersect_top = "+";
            this->style_t_intersect_bottom = "+";

            this->style_edge_topleft = "+";
            this->style_edge_topright = "+";
            this->style_edge_buttomleft = "+";
            this->style_edge_buttomright = "+";
            break;
        }
        case LINED: {
            this->style_line_horizontal = "━";
            this->style_line_vertical = "┃";
            this->style_line_cross = "╋";

            this->style_t_intersect_right = "┫";
            this->style_t_intersect_left = "┣";
            this->style_t_intersect_top = "┳";
            this->style_t_intersect_bottom = "┻";

            this->style_edge_topleft = "┏";
            this->style_edge_topright = "┓";
            this->style_edge_buttomleft = "┗";
            this->style_edge_buttomright = "┛";
            break;
        }
        case DOUBLE_LINE: {
            this->style_line_horizontal = "═";
            this->style_line_vertical = "║";
            this->style_line_cross = "╬";

            this->style_t_intersect_right = "╣";
            this->style_t_intersect_left = "╠";
            this->style_t_intersect_top = "╦";
            this->style_t_intersect_bottom = "╩";

            this->style_edge_topleft = "╔";
            this->style_edge_topright = "╗";
            this->style_edge_buttomleft = "╚";
            this->style_edge_buttomright = "╝";
            break;
        }
    }
}

ConsoleTableRow.ч

#ifndef CONSOLETABLE_CONSOLETABLEENTRY_H
#define CONSOLETABLE_CONSOLETABLEENTRY_H

#include <string>
#include <vector>

class ConsoleTableRow {
public:

    ConsoleTableRow(int width);

    void addEntry(std::string data, int column);

    void editEntry(std::string data, int column);

    std::vector <std::string> getEntry();

private:
    std::vector <std::string> row;
};


#endif //CONSOLETABLE_CONSOLETABLEENTRY_H

ConsoleTableRow.cpp

#include "ConsoleTableRow.h"

ConsoleTableRow::ConsoleTableRow(int width) {
    this->row.resize(width);
}

void ConsoleTableRow::addEntry(std::string data, int column) {
    row[column] = data;
}

std::vector<std::string> ConsoleTableRow::getEntry() {
    return this->row;
}

void ConsoleTableRow::editEntry(std::string data, int column) {
    this->row[column] = data;
}

ConsoleTableUtils.ч

#ifndef CONSOLETABLE_CONSOLETABLEUTILS_H
#define CONSOLETABLE_CONSOLETABLEUTILS_H

#include <string>
#include <sstream>

class ConsoleTableUtils {
public:

    std::string repeatString(std::string input, int n) const;

};


#endif //CONSOLETABLE_CONSOLETABLEUTILS_H

ConsoleTableUtils.cpp

#include "ConsoleTableUtils.h"

std::string ConsoleTableUtils::repeatString(std::string input, int n) const {
    std::ostringstream os;
    for (int i = 0; i < n; i++)
        os << input;
    return os.str();
}

Вот пример для генерации использовать классы. Этот пример будет генерировать таблицы, которые можно увидеть на скриншоте выше.

#include <iostream>
#include <unistd.h>
#include "ConsoleTable.h"

int main() {

    ConsoleTable ct(BASIC);
    ct.setPadding(1);

    ct.addColumn("Country");
    ct.addColumn("Name");
    ct.addColumn("Profession");
    ct.addColumn("Age");

    auto entry = new ConsoleTableRow(4);
    entry->addEntry("Germany", 0);
    entry->addEntry("Michael", 1);
    entry->addEntry("Computer Engineer", 2);
    entry->addEntry("19", 3);
    ct.addRow(entry);

    auto entry2 = new ConsoleTableRow(4);
    entry2->addEntry("England", 0);
    entry2->addEntry("Robert", 1);
    entry2->addEntry("Artist", 2);
    entry2->addEntry("34", 3);
    ct.addRow(entry2);

    auto entry3 = new ConsoleTableRow(4);
    entry3->addEntry("United Kingdom", 0);
    entry3->addEntry("Julia", 1);
    entry3->addEntry("Designer", 2);
    entry3->addEntry("42", 3);
    ct.addRow(entry3);

    auto entry4 = new ConsoleTableRow(4);
    entry4->addEntry("United Staates", 0);
    entry4->addEntry("Jo", 1);
    entry4->addEntry("Actor", 2);
    entry4->addEntry("21", 3);
    ct.addRow(entry4);

    // Print all entries
    ct.printTable();

    return 0;
}

Этого проекта можно найти на GitHub.



1275
11
задан 1 апреля 2018 в 11:04 Источник Поделиться
Комментарии
4 ответа

Я вижу ряд вещей, которые могут помочь вам улучшить ваш код.

Думаю, пользователя

Это редко первое предложение я делаю, но в данном случае, представляется особо важным. Проблема в том, что сложение строк, кажется, требуют, чтобы пользователю отслеживать количество столбцов и синтаксис довольно ясным. Вместо этого:

auto entry2 = new ConsoleTableRow(4);
entry2->addEntry("England", 0);
entry2->addEntry("Robert", 1);
entry2->addEntry("Artist", 2);
entry2->addEntry("34", 3);
ct.addRow(entry2);
ct.printTable();

Я бы очень предпочитают писать это:

ct += {"England", "Robert", "Artist", "34"};
std::cout << ct;

Несколько из указанных ниже предложения касаются того, как можно сделать это.

Используйте только необходимые #includeС

В #include <unistd.h> линия в основной программе не является необходимым и может быть безопасно удалены. Кроме того, <iostream> оно, безусловно, необходимо ConsoleTable.cpp но не в ConsoleTable.h потому что интерфейс не использует его (хотя реализация не делает.)

Подумать о глобальном enumС

В TableStyle и HorizontalSeparator enumтолько действительно иметь значение в пределах ConsoleTable класс. По этой причине, я бы поставил TableStyle внутри ConsoleTable класс и двигаться HorizontalSeparator enum к ConsoleTable.cpp файл реализации. Ничего за пределами этого класса должны прикоснуться к нему.

Быть последовательным с именованием

Говоря о TableStyleэто странно LINED (С D на конце), но DOUBLE_LINE (без D на конце). Это незначительная вещь, но такое несоответствие может раздражать пользователей класса.

Упростить код

В printHorizontalSeperator код довольно длинный, повторяющихся и трудно читать. Кроме того, есть только три типа линий: линии, средней и нижней. Я предлагаю вместо того, чтобы значительно упростить этот. Во-первых, давайте сделаем это намного проще, чтобы добавить стили. Я добавить это к ConsoleTable класс:

static constexpr std::string_view markers[3][11] {
{ "-","|",
"+","+","+",
"+","+","+",
"+","+","+"},
{ "━","┃",
"┏","┳","┓",
"┣","╋","┫",
"┗","┻","┛"},
{ "═","║",
"╔","╦","╗",
"╠","╬","╣",
"╚","╩","╝"},
};

Две вещи, чтобы отметить здесь. Во-первых, я использую C++17 и так std::string_view, что позволяет, чтобы это было constexprно если у вас нет этого, это просто достаточно, чтобы сделать из них простые const std::string вместо. Во-вторых, персонажи физически устроен делает его гораздо проще, чтобы визуально убедиться в том, что персонажи являются правильными.

Далее, я бы рекомендовал создание функции такой частный член:

std::string line(unsigned n) const;

Это создает в верхней, средней или нижней строки и возвращает одну строку. Вот как я написал это:

std::string ConsoleTable::line(unsigned n) const {
std::stringstream line;
n *= 3;
line << markers[linetype][2+n];
for (std::size_t i{0}; i < widths.size()-1; ++i) {
for (std::size_t j{0}; j < (widths[i] + padsize + padsize); ++j) {
line << markers[linetype][0];
}
line << markers[linetype][3+n];
}
for (std::size_t j{0}; j < (widths.back() + padsize + padsize); ++j) {
line << markers[linetype][0];
}
line << markers[linetype][4+n] << '\n';
return line.str();
}

Вот как частные переменные-члены данных объявляются:

std::size_t padsize;
Style linetype;
bool innerlines;
std::vector<std::string> header;
std::vector<std::size_t> widths;
std::vector<std::vector<std::string>> rows;

Как вы можете, вероятно, предположить, я переименовал TableStyle для Style и положить его в определение класса.

Использовать std::initializer_list чтобы упростить код

Читая код, казалось, что наиболее фундаментальной частью ConsoleTable не линия стиль (который тока конструктор использует), но вместо имен столбцов. Вот конструктор, который использует std::initializer_list чтобы значительно упростить код:

ConsoleTable::ConsoleTable(std::initializer_list<std::string> list) :
padsize{1},
linetype{BASIC},
innerlines{false},
header{list}
{
updateWidths();
}

Здесь я с помощью C++11 конструктор единый синтаксис (с {}) так это однозначно, что это не вызовы функций

Операторы перегрузки, чтобы сделать синтаксис чистым

Использование += оператор в предложенном выше примере вовсе не сложно написать:

ConsoleTable &ConsoleTable::operator+=(std::initializer_list<std::string> row) {
if (row.size() > widths.size()) {
throw std::invalid_argument{"appended row size must be same as header size"};
}
std::vector<std::string> r = std::vector<std::string>{row};
rows.push_back(r);
for (std::size_t i{0}; i < r.size(); ++i) {
widths[i] = std::max(r[i].size(), widths[i]);
}
return *this;
}

Этот код не использует явного динамического выделения памяти, но это, конечно, может с незначительными изменениями. Хорошая вещь заключается в том, что если вы хотите изменить это, это деталь, которая скрыта внутри реализации класса и интерфейс может оставаться неизменным.

Использовать const где можно

В printTable() функция не (и не должен) изменять базовую структуру данных и, следовательно, должны быть объявлены const. Использование const везде, где это практично, чтобы сделать так, это очень простой способ получить хорошее представление из вашего кода.

Использовать ostream &operator<< вместо printTable

Текущий код void ConsoleTable::printTable() но что бы больше смысла и более общего назначения будет перегружать себя ostream operator<< вместо. Это позволяет пользователю кода, прямой выход к std::cout или любым другим удобным ostream. Декларация выглядит так:

friend std::ostream &operator<<(std::ostream &out, const ConsoleTable &t); 

Реализация выглядит так:

std::ostream &operator<<(std::ostream &out, const ConsoleTable &t)
{
out << t.line(0);
t.printRow(out, t.header);
auto mid = t.line(1);
if (!t.innerlines) {
out << mid;
mid.erase();
}
for (const auto &row : t.rows) {
out << mid;
t.printRow(out, row);
}
return out << t.line(2);
}

Обратите внимание, что при этом используется собственная функция-член printRow() для печати каждого std::vector<std::string> для заголовка либо данные. Он также использует ранее показанный собственный член line функция для создания верхней, средней и нижней линиями (каждый раз) для печати. Кроме того, я добавил функцию, которая может быть включена или выключена с помощью булевой переменной innerlines. При установке trueона печатает строки между каждой строки данных, но при установке falseон опускает тех внутренних линий, а просто печатает каждое строковых данных без визуального разделителя. Я считаю, это выглядит чище и дает дополнительные данные, которые будут показаны на экране одновременно.

Закрепить классы

Там не кажется, быть сильной необходимости ConsoleTableRow класс здесь. Я предлагаю вместо того, чтобы сохранить строки внутренне такой:

std::vector<std::vector<std::string>> rows;

Так как это всего лишь деталь реализации, как только мы используем std::initializer_listкак уже упоминалось выше (то есть, внутреннее представление строки не являются частью интерфейса), всегда можно добавить, если очень нужно.

Использование пространств имен, где это уместно

В ConsoleTableUtils объекта не должна быть объектом вообще. Это должно быть пространство или просто сделать repeatString автономная функция, если она нужна вообще. Я бы сказал, что это не действительно необходимо, поскольку он может так же легко быть сделано с одним из std::string конструкторы напрямую. При необходимости, он может быть записан таким образом:

std::string operator*(const std::string &other, std::size_t repeats) {
std::string ret;
ret.reserve(other.size() * repeats);
for ( ; repeats; --repeats)
ret.append(other);
return ret;
}

Это дает нам удобный и интуитивно понятный синтаксис:

std::string foo{"foo"};
std::cout << "String test\n" << (foo * 3) << '\n';

Не пишите this->

В функциях-членах, добавив this-> везде только загромождает код и делает его труднее читать, потому что this-> является неявным.

Сочетать настройка параметров для удобства

Есть функция, которая называется setTableStyle которая принимает один параметр, который на самом деле просто Тип линии. Я бы утверждать, что "стиль" может включать, а также обивка, и в моем варианте, нужно ли печатать внутренние линии или нет. Наконец, имя setTableStyle является несколько избыточным, так как это функция-член таблицу. Я сократить, что style так Table подразумевается, потому что это таблица объектов, и set подразумевается при передаче значения. Вот как я напишу это:

void ConsoleTable::style(ConsoleTable::Style s, std::size_t padsize, bool innerlines) {
linetype = s;
padsize = padsize;
innerlines = innerlines;
}

1
ответ дан 3 апреля 2018 в 06:04 Источник Поделиться

Я предлагаю вам использовать свободно API-интерфейс для ConsoleTable класс.

Вы можете использовать unique_ptr не вместо голого указателя, то не беспокоиться об утечке памяти.

Я думаю repeatString функция не нужен класс, а пишите как одна функция в пространстве имен, как namespace console_tableЕсли вам нравится класс, пожалуйста, объявить repeatString как статическая функция для предотвращения лишних экземпляров.

Пожалуйста, используйте use сайта или typedef переименовать лучшее имя для std::vector<std::string> что-то вроде Rows. пожалуйста, см. В разделе когда следует использовать оператор typedef в C++?

Я думаю getEntry() должно быть правильно constи вернуться const reference.

Использовать перечислимый класс вместо enum.

Использовать const, когда это возможно, право const функции следовать этому правилу слишком.

Я предпочитаю отделить cout от ConsoleTable и использовать ostream вместо. потому что мы можем нарисовать таблицу в любом месте я хочу. например в файл.

Использовать const reference вход для std::string в этой ситуации.

В ConsoleTableRow правильный охранник имя, вы пишите #ifndef CONSOLETABLE_CONSOLETABLEENTRY_H вместо #ifndef CONSOLETABLE_CONSOLETABLEROW_H

Я предлагаю вам изменить TableStyle для struct и вводили ConsoleTable вместо того, чтобы использовать некоторый тип, и создать три стиля, как и выше по умолчанию при создании экземпляра этого struct. в этой ситуации, в вашей библиотеке было больше гибкости. и пользователи вашей библиотеки может создать свой любимый стиль.

4
ответ дан 2 апреля 2018 в 01:04 Источник Поделиться

Не успеваю все в подробностях, но вот некоторые вещи, которые я заметил, не упомянутые sorosh_sabz еще.


  • Нет необходимости использовать this-> в ваши функции-члены, это само собой подразумевается. Смотри , когда я должен сделать явное использование this указатель? для того, когда это действительно требуется.

  • Предпочитают использовать .at() вместо operator[] при обращении векторов. Е. Г. в ConsoleTableRow класс функций. Поскольку у вас нет прямого контроля над индексом (пользователь вводит его), данный показатель может быть вне диапазона. В .at() функция будет делать проверку границ для вас, в то время как operator[] не. Таким образом, используя operator[] рекомендуется только, когда вы можете быть на 100% уверены, что там не будет запрещен доступ.

  • Как sorosh_sabs упоминалось, вы должны использовать std::unique_ptr вместо сырых указателей, потому что у вас есть утечки памяти повсюду в настоящее время. Однако, я не понимаю, почему вы на самом деле нужно добавить указатели все. Почему бы просто не пройти ConsoleTableRow вместо std::unique_ptr<ConsoleTableRow> для ConsoleTable.addRow()?

  • Нужен более сухой. Е. Г. функция ConsoleTable.printHorizontalSeperator имеет большое дублирование кода.

3
ответ дан 3 апреля 2018 в 12:04 Источник Поделиться


  • Что такое ConsoleTableUtils класс? Он не имеет членов, помимо метода, который, вероятно, следует вместо функции.

  • Вместо TableStyle перечисление, его можно сделать в стиле типа, содержащий символы, которые будут использоваться (и предоставить некоторые стандартные стили). Тогда мы можем отделить содержание от форматирования (например, мы могли бы использовать различные стили для печати и для отображения на экране).

  • Почему я могу писать только std::cout? Не сделать это трудно, чтобы писать для других потоков.

  • Нет никакой проверки, что addRow дается ряд нужной длины. И нет std::initializer_list конструктор для создания строки как один-лайнер.

  • Если мы выводим "средний сепаратор" в начале каждой строки, мы не нужны к особому случаю последней строки:

    printHorizontalSeparator(maxWidths, SEPARATOR_TOP);
    printRow(headers);
    for (row: rows) {
    printHorizontalSeparator(maxWidths, SEPARATOR_MIDDLE);
    printRow(row);
    }
    printHorizontalSeparator(maxWidths, SEPARATOR_BOTTOM);

  • Сократить дублирование в printHorizontalSeparator: решить, какой из четырех персонажей вы будете использовать (начало, середина, и конец Креста), и после этого все три блоки кода сворачивать в одну.

  • Рассмотреть вопрос о поддержке других символьных (в частности, std::wstring это очень полезно).

  • Добавить поддержку для каждого столбца выравнивание (слева, справа, по центру или числовое).

  • Орфография - "сентябрявроман Самойленко играю"

1
ответ дан 3 апреля 2018 в 01:04 Источник Поделиться