class Console
-
Hallo,
Bevor ich mit meinen Konsole-Anwendungen weitermache, dachte ich mir, ich zeig die mal lieber vorher. Nicht, das ich mich in was verrenne.
Später will ich vielleicht zu einer nicht statischen Kasse, worestoreOriginal()
zum Destruktor werden kann, mit 'RenderLoop' und Zeitmessung.Die Kommentare, tja, was sollte noch und was kann weg?
Ber der Fehlerbehandlung habe ich es wohl auch etwas...Es sind 600 Zeilen. Wer darauf keine Lust hat, kein Ding
#pragma once /* please use Multi-Byte Character Set in Compiler! */ #define NOMINMAX #include <windows.h> #include <iostream> #include <string> #include <vector> #include <exception> // simple own CHAR_INFO class Char { public: char glyph = ' '; int fColor = 0; int bColor = 0; Char() = default; Char(const char ch, const int fgColor, const int bgColor = 0) : glyph(ch), fColor(fgColor), bColor(bgColor) {} Char(const int c, const int fgColor, const int bgColor = 0) : glyph(char(c)), fColor(fgColor), bColor(bgColor) {} bool operator==(const Char& ch) const // for simple sort combinations of Char { bool equal = false; if (glyph == ch.glyph && fColor == ch.fColor && bColor == ch.bColor) { equal = true; } if (fColor == ch.bColor && bColor == ch.fColor) { equal = true; } return equal; } bool operator!=(const Char& ch) const { return !(*this == ch); } static Char Default() { return Char(' ', 15, 0); // white, should be the original front color in console } CHAR_INFO toCHAR_INFO() const { CHAR_INFO ch; ch.Char.UnicodeChar = static_cast<WCHAR>(glyph); ch.Attributes = static_cast<WORD>(fColor + 16 * bColor); return ch; } void print() const { std::cout << '\n' << glyph << " " << int(glyph) << " " << fColor << " " << bColor; } }; // some enums namespace console { enum glyph { SPACE = 32, LIGHT = 176, MED = 177, DARK = 178, SOLID = 219, }; enum color { BLACK = 0, DGREY = 8, GREY = 7, WHITE = 15, YELLOW = 14, DYELLOW = 6, RED = 12, DRED = 4, MAGENTA = 13, DMAGENTA = 5, DBLUE = 1, BLUE = 9, DCYAN = 3, CYAN = 11, GREEN = 10, DGREEN = 2 }; enum background_mode { NONE = 0, BGROUND = 1, FGROUND = 2, F_BGROUND = 3, F_FGROUND = 4 }; } // some helpers for pausing console namespace { void ready() { std::cerr << "\nready_\n"; std::cin.sync(); std::cin.get(); } void _key() { std::cin.sync(); std::cin.get(); } } // class Console class Console { struct Info { struct Size { SHORT width = 0; SHORT height = 0; } size; Char windowChar; SMALL_RECT windowRect = {}; std::string windowTitle; CONSOLE_CURSOR_INFO cci; CONSOLE_FONT_INFOEX cfi; CONSOLE_SCREEN_BUFFER_INFO csbi; }; struct KeyState { bool pressed = false; bool released = false; bool held = false; }; struct ErrorState { bool state = false; std::string type; }; inline static Info current, orig; inline static HANDLE outHandle; //inline static HANDLE inHandle; // is currently not required // buffer will written in console inline static std::vector<Char> charBuffer, savedBuffer; // for keyboard input handling inline static std::size_t keyBufferSize; inline static std::vector<KeyState> keys; inline static std::vector<SHORT> newState, oldState; // some error handling inline static ErrorState err; inline static bool consoleCreated = false; public: static Console create(const int width, const int height, const int fontWidth = 0, const int fontHeight = 0) { Console console; if (consoleCreated) Error("console already created"); outHandle = GetStdHandle(STD_OUTPUT_HANDLE); if (outHandle == INVALID_HANDLE_VALUE) throw std::exception(); if (!getCCI(orig.cci)) throw std::exception(); if (!getCFI(orig.cfi)) throw std::exception(); if (!getCSBI(orig.csbi)) throw std::exception(); orig.size = { orig.csbi.srWindow.Right + 1, orig.csbi.srWindow.Bottom + 1 }; orig.windowRect = orig.csbi.srWindow; orig.windowChar = Char::Default(); current = orig; keyBufferSize = 256; keys.resize(keyBufferSize); newState.resize(keyBufferSize); oldState.resize(keyBufferSize); if (resize((SHORT)width, (SHORT)height, (SHORT)fontWidth, (SHORT)fontHeight)) { consoleCreated = true; return console; } else { restoreOriginal(); return console; } } static bool resize(const SHORT width, const SHORT height, SHORT fontWidth = 0, SHORT fontHeight = 0) { SMALL_RECT rect = { 0, 0, 1, 1 }; if (!SetConsoleWindowInfo(outHandle, TRUE, &rect)) throw std::exception(); // Set the size of the screenbuffer if (!SetConsoleScreenBufferSize(outHandle, { width, height })) return Error("SetConsoleScreenBufferSize"); // Assign screenbuffer to the console if (!SetConsoleActiveScreenBuffer(outHandle)) return Error("SetConsoleActiveScreenBuffer"); if (!fontHeight) fontHeight = fontWidth; if (!fontWidth && !fontHeight) { fontWidth = orig.cfi.dwFontSize.X; fontHeight = orig.cfi.dwFontSize.Y; } // Set the fontsize now that the screenbuffer has been assigned to the console current.cfi.cbSize = sizeof(current.cfi); current.cfi.nFont = 0; current.cfi.dwFontSize.X = fontWidth; current.cfi.dwFontSize.Y = fontHeight; current.cfi.FontFamily = FF_DONTCARE; current.cfi.FontWeight = FW_NORMAL; wcscpy_s(current.cfi.FaceName, L"Consolas"); if (!SetCurrentConsoleFontEx(outHandle, FALSE, ¤t.cfi)) return Error("SetCurrentConsoleFontEx"); // Get screenbuffer info and check the maximum allowed window size if (!GetConsoleScreenBufferInfo(outHandle, ¤t.csbi)) return Error("GetConsoleScreenBufferSize"); if (width > current.csbi.dwMaximumWindowSize.X) return Error("screen width / font width too big"); if (height > current.csbi.dwMaximumWindowSize.Y) return Error("screen height / font height too big"); // Set physical console window size current.windowRect = { 0, 0, width - 1, height - 1 }; if (!SetConsoleWindowInfo(outHandle, TRUE, ¤t.windowRect)) return Error("SetConsoleWindowInfo"); // Set console title current.windowTitle = "Console " + std::to_string(width) + "x" + std::to_string(height) + " " + std::to_string(fontWidth) + "x" + std::to_string(fontHeight); if (!SetConsoleTitle(current.windowTitle.c_str())) return Error("SetConsoleTitle"); current.size = { width, height }; charBuffer.resize(width * height, current.windowChar); savedBuffer = charBuffer; return true; } static void restoreOriginal() { SMALL_RECT rect = { 0, 0, 1, 1 }; if (!SetConsoleWindowInfo(outHandle, TRUE, &rect)) throw std::exception(); if (!SetConsoleScreenBufferSize(outHandle, { orig.size.width, orig.size.height })) throw std::exception(); if (!SetConsoleActiveScreenBuffer(outHandle)) throw std::exception(); if (!SetCurrentConsoleFontEx(outHandle, FALSE, &orig.cfi)) throw std::exception(); if (!SetConsoleWindowInfo(outHandle, TRUE, &orig.windowRect)) throw std::exception(); if (!SetConsoleTitle(orig.windowTitle.c_str())) throw std::exception(); if (!SetConsoleCursorInfo(outHandle, &orig.cci)) throw std::exception(); if (!SetConsoleCursorPosition(outHandle, { 0, 0 })) throw std::exception(); charBuffer.resize(orig.size.width * orig.size.height, orig.windowChar); savedBuffer = charBuffer; current = orig; consoleCreated = false; } static int width() { return static_cast<int>(current.size.width); } static int height() { return static_cast<int>(current.size.height); } static Char windowCh() { return current.windowChar; } static int windowColor() { return current.windowChar.bColor; } static KeyState getKey(const int keyCode) { return keys[static_cast<std::size_t>(keyCode)]; } static ErrorState error() { return err; } static bool writeBuffer() { const std::vector<CHAR_INFO> outbuffer = convertToCHAR_INFO(); const CHAR_INFO* output = outbuffer.data(); const COORD coord = { current.size.width, current.size.height }; if (!WriteConsoleOutput(outHandle, output, coord, { 0, 0 }, ¤t.windowRect)) return Error("WriteConsoleOutput"); else return true; } static void saveBuffer() { savedBuffer = charBuffer; } static void loadBuffer() { charBuffer = savedBuffer; } static void fillBuffer() { std::fill(charBuffer.begin(), charBuffer.end(), current.windowChar); } static void fill(const Char& ch) { current.windowChar = ch; fillBuffer(); } static void fill(const int fColor, const int bColor) { current.windowChar = { current.windowChar.glyph, fColor, bColor }; fillBuffer(); } static void clear() { fillBuffer(); writeBuffer(); } static bool setTitle(const std::string& title) { if (!SetConsoleTitle(title.c_str())) return Error("SetConsoleTitle"); else return true; } static bool hideCursor() { current.cci.bVisible = 0; if (!SetConsoleCursorInfo(outHandle, ¤t.cci)) return Error("SetConsoleCursorInfo"); else return true; } static bool showCursor() { current.cci.bVisible = 1; if (!SetConsoleCursorInfo(outHandle, ¤t.cci)) return Error("SetConsoleCursorInfo"); else return true; } static bool setCursorPos(const int x, const int y) { if (!SetConsoleCursorPosition(outHandle, { static_cast<SHORT>(x), static_cast<SHORT>(y) })) return Error("SetConsoleCursorPosition"); else return true; } static bool setCursorHome() { if (!setCursorPos(0, 0)) return Error("setCursorHome"); else return true; } // must call before getting keys static void handleKeyBoardInput() { for (std::size_t i = 0; i < keyBufferSize; ++i) { newState[i] = GetKeyState(i); keys[i].pressed = false; keys[i].released = false; if (newState[i] != oldState[i]) { if (newState[i] & 0x8000) { keys[i].pressed = !keys[i].held; keys[i].held = true; } else { keys[i].released = true; keys[i].held = false; } } oldState[i] = newState[i]; } } // drawing routines static void putChar(const int x, const int y, const Char& ch, const int backgroundMode = console::NONE) { if (x >= 0 && x < current.size.width && y >= 0 && y < current.size.height) { const std::size_t idx = toLinearIndex(x, y); Char buf = charBuffer[idx]; charBuffer[idx] = combine(ch, buf, backgroundMode); } } static Char getChar(const int x, const int y) { Char ch; if (x >= 0 && x < current.size.width && y >= 0 && y < current.size.height) ch = charBuffer[toLinearIndex(x, y)]; return ch; } static void printChar( const int x, const int y, const char ch, const int fgColor = console::GREY, const int bgColor = windowColor(), const int backgroundMode = console::NONE) { putChar(x, y, Char(ch, fgColor, bgColor), backgroundMode); } static void print( int x, const int y, const std::string& text, const int fgColor = console::GREY, const int bgColor = windowColor(), const int backgroundMode = console::NONE) { for (const auto& ch : text) printChar(x++, y, ch, fgColor, bgColor, backgroundMode); } static void clip(int& x, int& y) { if (x < 0) x = 0; if (x >= current.size.width) x = current.size.width; if (y < 0) y = 0; if (y >= current.size.height) y = current.size.height; } static void putLine(int x0, int y0, const int x, const int y, const Char& ch, const int backgroundMode = console::NONE) { const int dx = std::abs(x - x0); const int dy = -std::abs(y - y0); const int sx = x0 < x ? 1 : -1; const int sy = y0 < y ? 1 : -1; int ed = dx + dy; for (;;) { putChar(x0, y0, ch, backgroundMode); if (x0 == x && y0 == y) break; const int ed2 = 2 * ed; if (ed2 > dy) { ed += dy; x0 += sx; } if (ed2 < dx) { ed += dx; y0 += sy; } } } static void putRectangle( const int x0, const int y0, const int x4, const int y4, const Char& ch, const int backgroundMode = console::NONE) { putLine(x0, y0, x4, y0, ch, backgroundMode); putLine(x4, y0, x4, y4, ch, backgroundMode); putLine(x4, y4, x0, y4, ch, backgroundMode); putLine(x0, y4, x0, y0, ch, backgroundMode); } static void fillRectangle( const int x0, const int y0, const int x4, const int y4, const Char& ch, const int backgroundMode = console::NONE) { for (int x = x0; x < x4 + 1; x++) for (int y = y0; y < y4 + 1; y++) putChar(x, y, ch, backgroundMode); } // simple floodfill static void fillArea(const int x, const int y, const Char& ch, const int backgroundMode = console::NONE) { const int currCol = getChar(x, y).fColor; if (x >= 0 && x < current.size.width && y >= 0 && y < current.size.height) { if (currCol != ch.fColor) { putChar(x, y, ch, backgroundMode); fillArea(x, y + 1, ch, backgroundMode); fillArea(x, y - 1, ch, backgroundMode); fillArea(x - 1, y, ch, backgroundMode); fillArea(x + 1, y, ch, backgroundMode); } } } private: static std::size_t toLinearIndex(const int x, const int y) { return static_cast<std::size_t>(current.size.width * y + x); } static Char combine(const Char& lhs, const Char& rhs, const int backgroundMode) { Char ch = lhs; switch (backgroundMode) { case console::NONE: break; case console::BGROUND: ch.bColor = rhs.bColor; break; case console::FGROUND: ch.bColor = rhs.fColor; break; case console::F_BGROUND: ch.fColor = rhs.bColor; break; case console::F_FGROUND: ch.fColor = rhs.fColor; break; default: break; } return ch; } static std::vector<CHAR_INFO> convertToCHAR_INFO() { std::vector<CHAR_INFO> vec; for (const auto& ch : charBuffer) vec.push_back(ch.toCHAR_INFO()); return vec; } static bool Error(const std::string& errString) { err = { true, errString }; return false; } static bool getCSBI(CONSOLE_SCREEN_BUFFER_INFO& csbi) { if (!GetConsoleScreenBufferInfo(outHandle, &csbi)) return Error("GetConsoleScreenBufferInfo"); else return true; } static bool getCFI(CONSOLE_FONT_INFOEX& cfi) { cfi.cbSize = sizeof(cfi); if (!GetCurrentConsoleFontEx(outHandle, FALSE, &cfi)) return Error("GetCurrentConsoleFontEx"); else return true; } static bool getCCI(CONSOLE_CURSOR_INFO& cci) { if (!GetConsoleCursorInfo(outHandle, &cci)) return Error("GetConsoleCursorInfo"); else return true; } };
fürs Protokoll:
Ich habe die Tastatureingabe überarbeitet, deshalb klappt das nicht mehr in dem MandelbrotDings. Dafür müsste man es dort so ändern:bool getKey(const int keyCode, const bool singleStepMode) { bool move = false; if (singleStepMode) { if (Console::getKey(keyCode).pressed) move = true; } else { if (Console::getKey(keyCode).held) move = true; } return move; } void explore(Mandelbrot& mb, bool& exploring) { setCursorToZero(mb); const std::size_t iterStep = 10; // add this to iterations in addIterations const std::size_t zoomIterStep = 50; // add this to iterations in zoomToCursor const int zoomSteps = 30; // amount of zoomSteps in zoomToCursor bool singleStepMode = false; bool input = true; while (input) { rePlot(mb); Console::handleKeyBoardInput(); if (Console::getKey(VK_ESCAPE).pressed) { input = false; exploring = false; } if (Console::getKey(VK_SHIFT).pressed) { if (!singleStepMode) singleStepMode = true; else singleStepMode = false; } if (getKey(VK_RIGHT, singleStepMode)) cursorRight(mb); if (getKey(VK_LEFT, singleStepMode)) cursorLeft(mb); if (getKey(VK_UP, singleStepMode)) cursorUp(mb); if (getKey(VK_DOWN, singleStepMode)) cursorDown(mb); if (Console::getKey('Z').pressed) zoomToCursor(mb, zoomSteps, zoomIterStep); } }
-
@zeropage sagte in class Console:
/* please use Multi-Byte Character Set in Compiler! */
Wieso eigentlich?
Ich habe es gerade mal kurz ausprobiert, es sind eigentlich nur extrem wenige Änderungen nötig, um beide Konfigurationen zu unterstützen:// Char: TCHAR glyph; //... Char(TCHAR ch, int fgColor, int bgColor=0) : glyph(ch), fColor(fgColor), bColor(bgColor) {} // Info: tstring windowTitle; // Console: bool setTitle(const tstring& title) { // .. } void printChar( int x, int y, TCHAR ch, int fgColor, int bgColor, int backgroundMode=console::NONE) { putChar(x, y, Char(ch, fgColor, bgColor), backgroundMode); } void print( int x, int y, const tstring& text, int fgColor, int bgColor, int backgroundMode=console::NONE) { for (const auto& ch:text) printChar(x++, y, ch, fgColor, bgColor, backgroundMode); } // mit tstring: typedef std::basic_string<TCHAR> tstring;
Static hast du ja schon angessprochen und die Zeichenfunktionen sollten mMn freie Funktionen sein. Da könnten noch einige folgen, die die Klasse somit immer weiter aufblähen würden.
Größenänderungen scheinen ein Problem zu sein. Mit der Konsole mache ich recht wenig, meine mich aber zu erinnern, dass man dort auch eine Callbackfunktion registrieren kann, die allerdings nur bei Größenänderungen des Buffers aufgerufen wird.
Vielleicht wäre es besser den Stil des Fensters zu ändern, sodass der Benutzer die Fenstergröße nicht ändern kann.SetWindowLongPtr(GetConsoleWindow(), GWL_STYLE, GetWindowLongPtr(GetConsoleWindow(), GWL_STYLE) & ~(WS_THICKFRAME|WS_MAXIMIZEBOX));
Noch ein Edit: Wenn einem die Konsole nicht gehört oder das Programm per Verknüpfung gestartet wurde, müsste allerdings vorher überprüft werden, ob das Fenster (bereits) maximiert ist. Wenn ja, müsste das Fenster zuvor normal angezeigt werden. Unschön, geht aber wohl nicht anders.
fillRectangle zeichnet mMn zu weit, da andere Frameworks (z.B. GDI) die Spalte rechts und die Reihe unten nicht mehr zeichnen. Beim Zeichnen verschiedener Formen vertut man sich viel zu häufig um genau einen Pixel, sodass ich es bei deinem FillRect zumindest so wie Windows machen würde.
Bei einem Floodfill würde ich es zudem vermutlich nicht riskieren, diesen rekursiv zu implementieren. Iterativ ist er schneller (was hier zwar vermutlich egal ist) und auch nicht schwieriger zu implementieren.Das ist nur ein erster Eindruck. Vielleicht hilft es ja etwas.
Edit: Hier ist noch ein „Kunstwerk“, mit ausgegrauter Maximizebox, Unicodekompilierung und etwas zu großem fillRecangle.
-
@zeropage sagte in class Console:
bool operator==(const Char& ch) const // for simple sort combinations of Char { bool equal = false; if (glyph == ch.glyph && fColor == ch.fColor && bColor == ch.bColor) { equal = true; } if (fColor == ch.bColor && bColor == ch.fColor) { equal = true; } return equal; }
Huch?
Was soll das 2.if
da?
-
@hustbaer sagte in class Console:
Was soll das 2.
if
da?Im ersten
if
wird geprüft, ob alle drei Variablen gleich sind, im zweitenif
, ob nur Hintergrund- und Vordergrundfarbe vertauscht sind. Das ist zwar technisch ein Unterschied, aber praktisch, optisch sind es Gleiche.Da merke ich gerade, das der Kommentar nicht richtig passt, eigentlich sollen die Vergleiche nur helfen, Doppelte zu entfernen, also eine Vorstufe zum Sortieren.
Und jetzt merke ich, das das nur für wenige Zeichen gilt... Ein roter Kreis auf gelben Hintergrund ist was anderes als ein gelber Kreis auf roten Hintergrund... Mist... Ist lange her, das ich das gemacht habe.
-
@yahendrik sagte in class Console:
Wieso eigentlich?
Ich habe es gerade mal kurz ausprobiert, es sind eigentlich nur extrem wenige Änderungen nötig, um beide Konfigurationen zu unterstützen:Edit: Hier ist noch ein „Kunstwerk“, mit ausgegrauter Maximizebox, Unicodekompilierung und etwas zu großem fillRecangle.
Schick!
-
@zeropage sagte in class Console:
Im ersten if wird geprüft, ob alle drei Variablen gleich sind, im zweiten if, ob nur Hintergrund- und Vordergrundfarbe vertauscht sind. Das ist zwar technisch ein Unterschied, aber praktisch, optisch sind es Gleiche.
Naja da fehlt aber noch das
glyph == ch.glyph
. Sonst ist ein gelbes X auf rotem Hintergrund das selbe wie ein rotes Y auf gelbem Hintergrund. Und das ist ja wohl nicht gewollt.Und jetzt merke ich, das das nur für wenige Zeichen gilt... Ein roter Kreis auf gelben Hintergrund ist was anderes als ein gelber Kreis auf roten Hintergrund... Mist... Ist lange her, das ich das gemacht habe.
Ich wüsste nicht für welche Zeichen das überhaupt gilt - es ist immer das inverse Bild.
-
ps: Ich finde Projekte die nur mit MBCS oder nur mit UNICODE kompiliert werden können doof. Wenn der Code nur mit 8-Bit Strings umgehen kann, dann verwende einfach die ganzen
A
Funktionen/Klassen (SetWindowTextA
,CStringA
etc.).Und wenn der Code mit UTF-16 Strings umgehen kann, dann verwende die
W
Funktionen/Klassen.Dann ist es unabhängig davon wie man es baut - es funktioniert einfach.
Das ganze
TCHAR
Zeugs macht mMn. nur Sinn wenn man nicht-NT basierte Windows Versionen wie Win95, 98, 98 SE oder ME unterstützen will/muss aber gleichzeitig für NT basierte Windows Versionen Unicode Support haben möchte. Dann kann man mit TCHAR arbeiten und muss das Programm nur 1x für MBCS und 1x für Unicode übersetzen um beiden Versionen zu bekommen.Wenn man aber eh nur für NT-basierte Windows Versionen entwickelt (was ich heutzutage als Standard ansehen würde), dann kann man sich den ganzen
TCHAR
Ranz sparen.
-
Ja, die Vergleiche in
Char
muss ich wirklich überdenken. Da kann ich jetzt nicht wirklich argumentierenAber den Wechsel zu
TCHAR
undtstring
finde ich schon interessant. Jetzt fallen wenigstens die Fehlermeldungen weg, wenn man nicht umstellt. DaConsole::setTitle(std::string)
nur von außerhalb aufgerufen wird, habe ich dort nur einetoTstring()
Funktion eingefügt.Nur die Zeichentabelle sieht bei mir unter Unicode seltsam aus. Wahrscheinlich ist die Lösung relativ simpel, wo ich vielleicht bald selbst drauf komme, wollte die Unterschiede aber mal zeigen.
Multi-Byte:
https://ibb.co/Bf6YVvSUnicode:
https://ibb.co/tB4bccX
-
@zeropage sagte in class Console:
Aber den Wechsel zu TCHARund tstring finde ich schon interessant. Jetzt fallen wenigstens die Fehlermeldungen weg, wenn man nicht umstellt. Da Console::setTitle(std::string) nur von außerhalb aufgerufen wird, habe ich dort nur eine toTstring() Funktion eingefügt.
Tun sie eben nicht, wie du ja selbst bemerkt hast.
Sie würden wegfallen, wenn du fixchar
+A
verwenden würdest oder fixwchar_t
+W
.Nur die Zeichentabelle sieht bei mir unter Unicode seltsam aus.
Ja. Klar. Wundert dich das? Die ersten 256 Zeichen in Unicode sind halt nicht identisch mit den ersten (und einzigen) 256 Zeichen in Codepage 850 (oder was auch immer deine Console-Codepage ist).