В заметке предлагается набор классов 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, поэтому в этой части класс следует дорабатывать.

Комментарии (5)


  1. tzlom
    05.01.2018 15:41
    +2

    Если глянуть bprinter можно увидеть что буст — опциональная зависимость.
    И если бы я выбирал — выбрал бы bprinter. Почему? Потому что он оформлен гораздо лучше — есть репозиторий, заголовок отделён от реализации, есть мейк файл и демка, которые я могу быстро собрать и не менее быстро подключить к проекту.
    Если вы действительно хотите чтобы вашим кодом пользовались — оформляйте его так, чтобы пользоваться было удобно.


  1. StriganovSergey
    08.01.2018 03:10

    Может, я чего-то не понял, но если бы я выводил табличные данные данные, то конечно же — использовал бы вывод в XLSX.
    Разумеется, без необходимости наличия на компе установленного офиса, т.е. напрямую формировал бы файл.
    Ну, там же не сложный формат, чтобы реализовать его «врукопашную», или найти готовые исходники, ведь много кто такое уже делал, наверняка.
    Бегло взглянув на этот код, нашел пока только испугавшую меня строку с CreateInstance(«Excel.Application») — думал, так уже с экселом не работают в 2018 году.
    Ну, разве что — из какого-нибудь скрипта VBS, но уж точно не из плюсов.
    Как-то… странно все это.
    Иногда хочется спросить даже не почему, а за что так :)


    1. shtr Автор
      08.01.2018 07:16

      Причина, видимо, в том, что код писался примерно в 2008 году, а далее просто использовался без рефакторинга


    1. shtr Автор
      08.01.2018 12:32

      Также можно работать и с xls, а насчет xslx согласен, это работа с xml. Опять же все это упирается в давность написания кода


      1. StriganovSergey
        08.01.2018 13:20

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