Pull to refresh

Вывод табличных данных в консоль, файл или MS Excel в стиле потоков C++

Reading time9 min
Views48K
В заметке предлагается набор классов C++ (работоспособность проверена в VS2008 и VS 2013; используется только С++03 и STL) для вывода в поток std::ostream данных с табличным форматированием. Распространяется «As Is».

    st << "#" << "Property" << "Value" << "Unit";
    enum {nr = 10};
    for (int i = 0; i < nr; i++) {
        st << i + 1 << "Prop" << i << "Unit";
    }

Результат будет выглядеть так:



Уже в процессе подготовки заметки нашел похожий проект bprinter (подробного сравнения не проводил; прослеживается зависимость от Boost::Spirit, что не всегда удобно), который является аналогом моего файла

StreamTable.h
#ifndef __STREAM_TABLE_H
#define __STREAM_TABLE_H

#undef max
#undef min

#include <string>
#include <vector>
#include <algorithm>
#include <iostream>

//стратегия формирования единой таблицы
#define CRLF "\n"

//стратегия построчной выгрузки таблицы
//#define CRLF std::endl

/**
* Прямоугольная таблица с разделителями строк и столбцов
* Синтаксис как у потоков C++
*/
class StreamTable {
public:
    std::ostream &os_;

    StreamTable(std::ostream &os = std::cout, char delimRow = ' ', char delimCol = ' ') :
        borderExtOn_(true),
        delimRowOn_(true),
        delimRow_(delimRow),
        delimColOn_(true),
        delimCol_(delimCol),
        os_(os),
        colIndex_(0),
        firstCell_(1) {}

    virtual ~StreamTable() {}

    virtual std::ostream &os() const {
        return os_;
    }

    //отображать внешние границы?
    void MakeBorderExt(bool on) {
        borderExtOn_ = on;
    }

    //символ разделителя строк
    void SetDelimRow(bool delimOn, char delimRow = ' ') {
        delimRowOn_ = delimOn;
        if (delimRowOn_)
            delimRow_ = delimRow;
        else if (!delimColOn_)
            MakeBorderExt(false);
    }

    //символ разделителя столбцов
    void SetDelimCol(bool delimOn, char delimCol = ' ') {
        delimColOn_ = delimOn;
        if (delimColOn_)
            delimCol_ = delimCol;
        else if (!delimRowOn_)
            MakeBorderExt(false);
    }

    int AddCol(int colWidth, bool visible = true) {
        colWidth_.push_back(colWidth);
        visible_.push_back(visible);
        return colWidth_.back();
    }

    void SetVisible(int col, bool flg) {
        visible_[col - 1] = flg;
    }

    void SetCols(int colCount, int colWidth = 0) {
        Clear();

        for (int ic = 0; ic < colCount; ic++) {
            AddCol(colWidth);
        }
    }

    virtual void Clear() {
        colWidth_.clear();
        visible_.clear();
        colIndex_ = 0;
        firstCell_ = 1;
    }

    void AddEmptyRow() {
        for (int ic = 0; ic < (int)colWidth_.size(); ic++) {
            *this << "";
        }
    }

    template <typename T> StreamTable &operator << (const T &obj) {
        Push(obj);
        return *this;
    }

    StreamTable &operator << (const std::string &s) {
        colWidth_[colIndex_] = std::max(colWidth_[colIndex_], (int)s.size() + 1);
        Push(s);
        return *this;
    }

    StreamTable &operator << (const char *s) {
        colWidth_[colIndex_] = std::max(colWidth_[colIndex_], (int)strlen(s) + 1);
        Push(s);
        return *this;
    }

protected:
    int colIndex_;

private:
    bool borderExtOn_;
    bool delimRowOn_;
    char delimRow_;

    bool delimColOn_;
    char delimCol_;

    std::vector<int> colWidth_;
    bool firstCell_;
    std::vector<int> visible_;

    template <typename T>
    void Push(const T &obj) {
        if (firstCell_) {
            if (borderExtOn_)
                MakeRowBorder();

            firstCell_ = 0;
        }

        if (visible_[colIndex_]) {
            DelimCol();

            os_.width(colWidth_[colIndex_]);
            os_.fill(' ');
            os_ << /*std::setiosflags(std::ios::left) << */obj;
        }

        if (++colIndex_ == (int)colWidth_.size()) {
            DelimCol();

            if (delimRowOn_)
                MakeRowBorder();
            else
                os_ << CRLF;

            colIndex_ = 0;
        }
    }

    void MakeRowBorder() {
        os_ << CRLF;
        DelimCol();

        int ic;
        for (ic = 0; ic < (int)colWidth_.size(); ic++) {
            if (visible_[ic]) {
                os_.width(colWidth_[ic] + 1);
                os_.fill(delimRow_);
                DelimCol();
            }
        }
        os_ << CRLF;
    }

    void DelimCol() {
        if (delimColOn_ && (borderExtOn_ || colIndex_))
            os_ << delimCol_;
        else
            os_ << ' ';
    }

    //запрет на копирование
    StreamTable &operator = (const StreamTable &);
};

#endif // __STREAM_TABLE_H


Класс StreamTable позволяет построчно выводить табличные данные в формате потоков С++. Принимает в конструкторе ссылку на std::ostream, так что помимо std::cout (по-умолчанию) можно осуществлять запись в файл, передав std::ofstream &. Полезен при формировании лог-файла с результатами расчета.

#include <sstream>
#include "StreamTable.h"

void TestStreamTable1()
{
    StreamTable st(std::cout);
    st.AddCol(5);
    st.AddCol(15);
    st.AddCol(10);
    st.AddCol(10);

    //разкомментировать, если столбцы имеют одинаковую толщину
    //st.Clear();
    //st.SetCols(4, 10);

    //st.SetVisible(1, false);//столбцы можно скрывать

    st.MakeBorderExt(true);
    st.SetDelimRow(true, '-');//st.SetDelimRow(false);//без символов-разделителей строк
    st.SetDelimCol(true, '|');//st.SetDelimCol(false);//без символов-разделителей строк

    //заголовок и значения выводятся одинаково
    st << "#" << "Property" << "Value" << "Unit";
    enum {nr = 10};
    for (int i = 0; i < nr; i++) {
        st << i + 1 << "Prop" << i << "Unit";
    }
}

Для возможности использования StreamTable для выгрузки в MS Excel был реализован специальный поток excelstream и вспомогательный класс для работы с MS Excel:

Файл MSExcel.h
#pragma once

#include <sstream>

#import "C:\Program Files (x86)\Common Files\Microsoft Shared\Office12\MSO.DLL" \
rename("RGB","_RGB")\
rename("DocumentProperties", "_DocumentProperties")\
rename("SearchPath","_SearchPath")

#import "C:\Program Files (x86)\Common Files\Microsoft Shared\VBA\VBA6\VBE6EXT.OLB"

#import "C:\Program Files (x86)\Microsoft Office\Office12\EXCEL.EXE" \
rename("DialogBox","_DialogBox") \
rename("RGB","_RGB") \
rename("CopyFile", "_CopyFile") \
rename("ReplaceText", "_ReplaceText") \
no_auto_exclude

#define THROW(msg) throw std::exception(msg);

/**
 *
 */
class ExcelLoader {
public:
    virtual ~ExcelLoader() {
        Close();
    }

    Excel::_ApplicationPtr excel_;
    Excel::_WorksheetPtr sheet_;
    Excel::RangePtr range_;

    //отвязываение от Excel, чтобы решение о закрытии и сохранении принял пользователь
    void Detach() {
        if (!excel_)
            return;

        range_.Detach();
        range_ = 0;

        sheet_.Detach();
        sheet_ = 0;

        excel_.Detach();
        excel_ = 0;

        ::CoUninitialize();
    }

    //закрывает без сохранения ранее открытую книгу через LoadExcel
    void Close() {
        if (!excel_)
            return;

        try {
            excel_->DisplayAlerts[0] = false;
            excel_->Quit();
            excel_->DisplayAlerts[0] = true;

            Detach();

            //todo: с MS Excel 2010 обнаружилось, что CoUninitialize не всегда закрывает процесс
            std::system("taskkill /F /IM Excel.exe");

            //ожидаем закрытия процесса EXCEL
            while (FindWindow("XLMAIN", NULL)) {};
        } catch (_com_error &er) {
            THROW(er.ErrorMessage());
        }
    }

    //сохраняет ранее открытую книгу через LoadExcel
    void Save() {
        if (!excel_)
            return;

        excel_->DisplayAlerts[0] = false;
        try {
            excel_->Save();
        } catch (_com_error &er) {
            THROW(er.ErrorMessage());
        }
        excel_->DisplayAlerts[0] = true;
    }

    //инстанцирует Excel и открывает лист с индексом ws_index (>= 1) в книге fname
    void LoadExcel(const std::string &fname, int ws_index) {
        if (FAILED(::CoInitialize(NULL)))
            THROW("CoInitialize failure");

        if (FAILED(excel_.CreateInstance("Excel.Application"))) {
            std::stringstream ss;
            ss << "CreateInstance failed: " << GetLastError();
            std::string msg = ss.str();
            THROW(msg.c_str());
        }

        excel_->Visible[0] = TRUE;

        Excel::_WorkbookPtr book = excel_->Workbooks->Open(fname.c_str());
        if (!book)
            THROW(std::string("Can't open ").append(fname).append(": Workbooks->Open method failed").c_str());

        sheet_ = excel_->ActiveSheet;
        if (ws_index < 1 || excel_->Sheets->Count < ws_index)
            THROW("ws_index_ must be in [1, Sheets.Count]");

        sheet_ = excel_->Sheets->Item[ws_index];

        if (!sheet_)
            THROW("Failed to get a pointer to the active sheet");

        range_ = sheet_->Cells;
        if (!range_)
            THROW("Failed to get a pointer to the cells on the active sheet");
    }
};


Файл ExcelStream.h
#pragma once

#include <sstream>
#include "MSExcel.h"

typedef char CharT;
typedef std::char_traits<CharT> TraitsT;

class excel_stringbuf : public std::basic_stringbuf<CharT, TraitsT> {
public:
    static const char colDelim = '\t';
    static const char rowDelim = '\n';

    virtual ~excel_stringbuf() {
        sync();
    }

    void SetRange(Excel::RangePtr pRange, int irow_offset, int icol_offset) {
        rng_ = pRange;
        //запись будем производить из верхнего левого узла листа
        irow_ = irow_offset;

        icol_offset_ = icol_offset;
        icol_ = icol_offset;
    }

    Excel::RangePtr GetRange() const {
        return rng_;
    }

    int &CurRow() {
        return irow_;
    }

    int &CurCol() {
        return icol_;
    }

    int sync() {
        output_string(str().c_str());
        str(std::basic_string<CharT>());//очистка текстового буфера
        return 0;
    }

protected:
    Excel::RangePtr rng_;
    int irow_;

    int icol_offset_;
    int icol_;

    //в отличие от библиотечной isspace не учитывает пробел в качестве разделителя
    bool IsSpace(char c) const {
        return (c == colDelim) || (c == rowDelim) || (c == '\r');
    }

    /**
     * Записывает строку s в файл Excel.
     * При этом признаком перехода к следующему столбцу является символ '\t',
     * а к следующей строке - '\n'.
     * Пример: строка вида "1\t2\n3\t4 5" будет записана в виде
     *		(1,1) = 1
     *		(1,2) = 2
     *		(2,1) = 3
     *		(2,2) = 4 5,
     * где (i,j) - координаты ячейки
     * todo: добавить обработку переполнения количества строк/столбцов
     */
    void output_string(const std::string &s);
};

void
excel_stringbuf::output_string(const std::string &s)
{
    //плавающий указатель на символы строки, отличные от пробельных (\n \t)
    std::string::const_iterator be = s.begin();
    std::string::const_iterator en = s.end();
    std::string::const_iterator it = be;

    while (it != en) {
        bool dump = false;
        bool isTab = false;
        bool isEnd = false;

        //если встретился символ перевода столбца или строки определяем,
        //необходимо ли произвести запись в текущую ячейку (iRow, iCol)

        if (*it == colDelim) {
            isTab = true;
            dump = !IsSpace(*be);
        } else if (*it == rowDelim) {
            isEnd = true;
            dump = !IsSpace(*be);
        } else {
            //как только встретился не разделительный символ, выставить на него be
            if (IsSpace(*be))
                be = it;

            if (it + 1 == en) {
                //прочли последний символ и он не пробельный
                dump = true;
                //при записи в ячейку предполагается, что
                //it указывает на разделительный символ
                it = en;
            }
        }

        if (dump) {
            //записать в текущую ячейку часть строки из промежутка [be, it)
            const std::string &item = s.substr(be - s.begin(), it - be);
            rng_->Item[irow_][icol_] = _variant_t(item.c_str());
        }

        //обновляем координаты положения в файле следующей строки
        if (isTab) {
            icol_++;
            be = it;
        } else if (isEnd) {
            irow_++;
            icol_ = icol_offset_;
            be = it;
        }

        if (it == en) {
            //достигли конца строки
            break;
        } else
            it++;
    }
}

/**
 * Класс для записи в Excel с синтаксисом как у STL потоков
 */
class excelstream : public std::basic_ostream<CharT, TraitsT> {
public:
    excelstream(Excel::RangePtr &rng, int irow_offset = 1, int icol_offset = 1)
        : std::basic_ostream<CharT, TraitsT>(&buf_) {
        buf_.SetRange(rng, irow_offset, icol_offset);
    }

    virtual ~excelstream() {
        flush();
    }

private:
    excel_stringbuf buf_;
};


Внимание, поскольку в MSExcel.h используется #import, то для компиляции необходимо прописать актуальные пути к MSO.DLL, VBE6EXT.OLB, EXCEL.EXE в соответствии с установленной версией MS Office.

#include <sstream>
#include "StreamTable.h"
#include "ExcelStream.h"

void TestStreamTable2()
{
    //открываем
    ExcelLoader xls;
    xls.LoadExcel("C:/log.xlsx", 1);

    excelstream os(xls.range_, 1, 1);//координаты левого верхнего угла

    StreamTable st(os);
    st.SetCols(4);//todo: задание ширины Excel-ячеек пока не реализовано

    st.MakeBorderExt(false);//обязательно, иначе будут лишние пустые строки
    st.SetDelimRow(false);//обязательно, иначе будут лишние пустые строки
    st.SetDelimCol(true, excel_stringbuf::colDelim);//обязательно, т.к. excel_stringbuf разбивает строку по столбцам с учетом colDelim

    //заголовок таблицы
    st << "#" << "Property" << "Value" << "Unit";

    enum { nr = 10 };
    for (int i = 0; i < nr; i++) {
        st << i + 1 << "Prop" << i << "Unit";
    }

    os.flush();
    xls.Detach();//после Detach вызов os или st некорректно
}

Поток excelstream в составе StreamTable работает таким образом, что его буфер парсит строку выгрузки, в которой символ '\t' интерпретируется как переход к столбцу вправо, а '\n' — как символ перехода на следующую строку. Пример ниже аналогичен TestStreamTable2 по результату и показывает принцип формирования такой строки.

#include <sstream>
#include "ExcelStream.h"

void TestStreamTable3()
{
    ExcelLoader xls;
    xls.LoadExcel("C:/log.xlsx", 1);

    excelstream os(xls.range_, 1, 1);
    os << "#\tProperty\tValue\tUnit\n";

    std::stringstream ss;

    enum { nr = 10 };
    for (int i = 0; i < nr; i++) {
        os << i + 1 << "\tProp\t" << i << "\tUnit\n";
    }

    os.flush();
    xls.Detach();//после Detach вызов os или st некорректно
}



Файл со всеми примерами main.cpp
#include <sstream>
#include "StreamTable.h"
#include "ExcelStream.h"

void TestStreamTable1()
{
    StreamTable st(std::cout);
    st.AddCol(5);
    st.AddCol(15);
    st.AddCol(10);
    st.AddCol(10);

    //разкомментировать, если столбцы имеют одинаковую толщину
    //st.Clear();
    //st.SetCols(4, 10);

    //st.SetVisible(1, false);//столбцы можно скрывать

    st.MakeBorderExt(true);
    st.SetDelimRow(true, '-');//st.SetDelimRow(false);//без символов-разделителей строк
    st.SetDelimCol(false, '|');//st.SetDelimCol(false);//без символов-разделителей строк

    //заголовок и значения выводятся одинаково
    st << "#" << "Property" << "Value" << "Unit";
    enum {nr = 10};
    for (int i = 0; i < nr; i++) {
        st << i + 1 << "Prop" << i << "Unit";
    }
}

void TestStreamTable2()
{
    //открываем
    ExcelLoader xls;
    xls.LoadExcel("C:/log.xlsx", 1);

    excelstream os(xls.range_, 1, 1);//координаты левого верхнего угла

    StreamTable st(os);
    st.SetCols(4);//todo: задание ширины Excel-ячеек пока не реализовано

    st.MakeBorderExt(false);//обязательно, иначе будут лишние пустые строки
    st.SetDelimRow(false);//обязательно, иначе будут лишние пустые строки
    st.SetDelimCol(true, excel_stringbuf::colDelim);//обязательно, т.к. excel_stringbuf разбивает строку по столбцам с учетом colDelim

    //заголовок таблицы
    st << "#" << "Property" << "Value" << "Unit";

    enum { nr = 10 };
    for (int i = 0; i < nr; i++) {
        st << i + 1 << "Prop" << i << "Unit";
    }

    os.flush();
    xls.Detach();//после Detach вызов os или st некорректно
}

void TestStreamTable3()
{
    ExcelLoader xls;
    xls.LoadExcel("C:/log.xlsx", 1);

    excelstream os(xls.range_, 1, 1);
    os << "#\tProperty\tValue\tUnit\n";

    std::stringstream ss;

    enum { nr = 10 };
    for (int i = 0; i < nr; i++) {
        os << i + 1 << "\tProp\t" << i << "\tUnit\n";
    }

    os.flush();
    xls.Detach();//после Detach вызов os или st некорректно
}

void main()
{
    try {
        TestStreamTable1();
        TestStreamTable2();
        TestStreamTable3();
    } catch(std::exception &ex) {
        std::cout << ex.what();
    }
}



В заключение отмечу, что в отношении excelstream не проверялась его работа как полиморфного указателя на std::ostream, поэтому в этой части класс следует дорабатывать.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 11: ↑8 and ↓3+5
Comments6

Articles