В заметке предлагается набор классов C++ (работоспособность проверена в VS2008 и VS 2013; используется только С++03 и STL) для вывода в поток std::ostream данных с табличным форматированием. Распространяется «As Is».
Результат будет выглядеть так:
Уже в процессе подготовки заметки нашел похожий проект bprinter (подробного сравнения не проводил; прослеживается зависимость от Boost::Spirit, что не всегда удобно), который является аналогом моего файла
Класс StreamTable позволяет построчно выводить табличные данные в формате потоков С++. Принимает в конструкторе ссылку на std::ostream, так что помимо std::cout (по-умолчанию) можно осуществлять запись в файл, передав std::ofstream &. Полезен при формировании лог-файла с результатами расчета.
Для возможности использования StreamTable для выгрузки в MS Excel был реализован специальный поток excelstream и вспомогательный класс для работы с MS Excel:
Внимание, поскольку в MSExcel.h используется #import, то для компиляции необходимо прописать актуальные пути к MSO.DLL, VBE6EXT.OLB, EXCEL.EXE в соответствии с установленной версией MS Office.
Поток excelstream в составе StreamTable работает таким образом, что его буфер парсит строку выгрузки, в которой символ '\t' интерпретируется как переход к столбцу вправо, а '\n' — как символ перехода на следующую строку. Пример ниже аналогичен TestStreamTable2 по результату и показывает принцип формирования такой строки.
В заключение отмечу, что в отношении excelstream не проверялась его работа как полиморфного указателя на std::ostream, поэтому в этой части класс следует дорабатывать.
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, поэтому в этой части класс следует дорабатывать.