Die Windows Konsole
Allgemeines
Speziell Anfänger in der Programmierung wünschen es sich, die öde Textkonsole wenigstens irgendwie farbig zu machen, da es schon schlimm genug ist, nur mit Text arbeiten zu können.
In diesem Tutorial wird eine Klasse entwickelt, mit der eben diese Einfärbung von Text aber auch vieles andere Möglich ist. So kann zum Beispiel der Titel des Konsolenfensters geändert und die Konsole kann in den Vollbildmodus versetzt werden. Außerdem ist es möglich, Text an einer bestimmten Position im Fenster zu zeichnen und sogar flimmerfrei zu überzeichnen, da nur die Zeichen, die geändert werden müssen, ersetzt werden.
Die Klasse
Hier steht nun alle Deklarationen. Die Erklärungen und Definitionen folgen dann Schritt für Schritt. Hier kann man erst einmal an den Methodennamen ablesen, was die Klasse alles zu bieten hat.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
#include <windows.h>
#include <tchar.h>
enum ConsoleColor
{
CC_Blue = FOREGROUND_BLUE,
CC_Green = FOREGROUND_GREEN,
CC_Red = FOREGROUND_RED,
CC_Intensity = FOREGROUND_INTENSITY
};
class Console
{
public:
bool Init();
bool SetTitle(const TCHAR* Title);
bool GetTitle(TCHAR* Title, DWORD Size) const;
bool SetFullScreenState(bool FullScreen);
bool GetFullScreenState() const;
bool SetCursorPosition(SHORT X, SHORT Y);
COORD GetCursorPosition() const;
bool SetCursorSize(DWORD Size);
DWORD GetCursorSize() const;
bool Write(SHORT X, SHORT Y, const TCHAR* String, WORD Color = CC_Blue | CC_Green | CC_Red | CC_Intensity);
bool Write(SHORT X, SHORT Y, const TCHAR* String, const WORD* Color);
bool Color(SHORT X, SHORT Y, WORD Color, DWORD Length);
bool Color(SHORT X, SHORT Y, const WORD* Color, DWORD Length);
bool ClearScreen();
static Console& Instance()
{
static Console Instance;
return Instance;
}
private:
HANDLE Handle;
bool FullScreenActivated;
Console();
};
|
Was auffällt, ist wohl die Inkludierung von tchar.h und die Verwendung von TCHAR. Dies soll die Kompatibilität zu sowohl Ansi als auch Unicode gewährleisten. Verwendet man Ansi, wird TCHAR zu char, verwendet man Unicode, wird TCHAR zu wchar_t.
Die Klasse ist ein Singleton, das heißt dass nur eine einzige Instanz von ihr existiert. Diese kann man mit der letzten Methode bekommen.
Der Konstruktor
Im Kontruktor wird lediglich das Handle der Konsole auf NULL, sowie die Variable, die anzeigt ob der Vollbildmodus aktiviert ist, auf false gesetzt.
|
C-/C++-Quelltext
|
1
2
3
|
Console::Console(void) : Handle(NULL), FullScreenActivated(false)
{
}
|
Init
Diese Methode muss vor allen anderen aufgerufen werden, da hier das Handle des Konsolenfensters beschafft wird, welches in fast jeder anderen Methode benötigt wird.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
|
bool Console::Init()
{
Handle = GetStdHandle(STD_OUTPUT_HANDLE);
return (Handle && Handle != INVALID_HANDLE_VALUE);
}
|
Zuerst wird hier also das Handle mit der Funktion GetStdHandle ermittelt und gleich danach auf Gültigkeit überprüft. Ist es weder NULL noch hat es den Wert INVALID_HANDLE_VALUE ist es gültig und es wird true zurückgegeben.
SetTitle
Diese Funktion setzt den Titel des Konsolenfensters und erwartet als einzigen Parameter eben jenen.
|
C-/C++-Quelltext
|
1
2
3
4
|
bool Console::SetTitle(const TCHAR* Title)
{
return (SetConsoleTitle(Title) == TRUE);
}
|
Hier wird lediglich die Funktion SetConsoleTitle aufgerufen, welche dann den Titel des Konsolenfensters setzt.
GetTitle
Hier folgt nun das Gegenstück zu SetTitle: GetTitle. Diese Methode liefert den Titel des Konsolenfensters und erwartet als ersten Parameter einen Zeiger auf den Speicherbereich, in den der Titel geschrieben wird und als zweiten Parameter die maximale Zeichenanzahl, die dieser fassen kann.
|
C-/C++-Quelltext
|
1
2
3
4
|
bool Console::GetTitle(TCHAR* Title, DWORD Size) const
{
return (GetConsoleTitle(Title, Size) == TRUE);
}
|
SetFullScreenState
Die Konsole kann mit der Tastenkombination Alt+Enter in den Vollbildmodus und wieder zurück versetzt werden. Der Druck dieser Tasten wird in dieser Methode simuliert.
Der Einzige Parameter ist eine Variable vom Typ bool, mit der man festlegt, ob der Vollbildmodus aktiviert oder deaktivert werden soll.
Nun gibt es leider ein kleines Problem: Windows Vista unterstüzt den Vollbildmodus der Konsole nicht mehr. Versucht man es doch, kommt eine MessageBox, die eben jenes verkündet. Deshalb wird zu Beginn der Methode abgefragt, ob es sich bei dem Betriebssystem um Vista handelt. Falls ja, wird die Methode abgebrochen und false zurückgegeben.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
bool Console::SetFullScreenState(bool FullScreen)
{
OSVERSIONINFO Info;
Info.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
if (!GetVersionEx(&Info))
return false;
if (Info.dwMajorVersion == 6 && Info.dwMinorVersion == 0 && Info.dwPlatformId == VER_PLATFORM_WIN32_NT)
return false;
if (FullScreen == FullScreenActivated)
return false;
keybd_event(VK_MENU , 0x38 , 0 , 0);
keybd_event(VK_RETURN , 0x1C , 0 , 0);
keybd_event(VK_MENU , 0x38 , KEYEVENTF_KEYUP , 0);
keybd_event(VK_RETURN , 0x1C , KEYEVENTF_KEYUP , 0);
FullScreenActivated = FullScreen;
return true;
}
|
Zu Beginn also wie erwähnt die Abfrage des Betriebssystems. Handelt es sich nicht um Vista, geht es weiter mit dem Vergleich der Variablen FullScreen und FullScreenActivated. Haben diese den gleichen wert, wird false zurückgegeben. Denn wenn der Vollbildmodus aktiviert werden soll (FullScreen ist also true), die Konsole aber bereits im Vollbildmodus ist (FullScreenActivated ist also ebenfalls true), würde eine Simulation der Eingabe den Vollbildmodus deaktivieren, was ja nicht geplant ist. Umgekehrt gilt natürlich das Gleiche.rde eine Simulation der Eingabe sie wieder in den Normalen Modus versetzen, was ja mit der Übergabe von true nicht geplant ist. In diesem Fall wird false zurückgegeben. Selbiges gilt natürlich auch umgekehrt.
Nach diesem Vergleich findet die eigentliche Simulation der Eingabe sowie das Setzen der Membervariable FullScreenActivated auf den Wert von FullScreen.
GetFullScreenState
Mit dieser Methode kann überprüft werden, ob der Vollbildmodus aktiviert ist. Dazu wird einfach FullScreenActivated zurückgegeben.
|
C-/C++-Quelltext
|
1
2
3
4
|
bool Console::GetFullScreenState(void) const
{
return FullScreenActivated;
}
|
SetCursorPosition
Auch diese Methode ist sehr simpel aufgebaut: Es werden als Parameter die X- und die Y-Position der gewünschten Cursorposition erwartet. Diese Werte werden dann in eine Struktur des Typs COORD eingesetzt und an SetConsoleCursorPosition übergeben.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
|
bool Console::SetCursorPosition(SHORT X, SHORT Y)
{
COORD Position = {X, Y};
return (SetConsoleCursorPosition(Handle, Position) == TRUE);
}
|
GetCursorPosition
Diese Methode ermittelt die aktuelle Position des Cursors mithilfe der Funktion GetConsoleScreenBufferInfo und liefert diese zurück. Schlägt die Funktion fehl, wird eine COORD-Struktur zurückgegeben, deren X- und Y-Wert jeweils -1 ist.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
|
COORD Console::GetCursorPosition() const
{
CONSOLE_SCREEN_BUFFER_INFO Info;
COORD Error = {-1, -1};
if (!GetConsoleScreenBufferInfo(Handle, &Info))
return Error;
return Info.dwCursorPosition;
}
|
SetCursorSize
Mit dieser Methode kann man die Größe des Cursors festlegen. Übergibt man als gewünschte Größe einen Wert der kleiner als 1 oder größer als 100 ist, wird der Cursor unsichtbar.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
bool Console::SetCursorSize(DWORD Size)
{
CONSOLE_CURSOR_INFO Info;
if (Size < 1 || Size > 100)
{
Info.dwSize = 1;
Info.bVisible = FALSE;
}
else
{
Info.dwSize = Size;
Info.bVisible = TRUE;
}
return (SetConsoleCursorInfo(Handle, &Info) == TRUE);
}
|
Die Struktur CONSOLE_CURSOR_INFO beinhaltet die Größe und die Sichtbarkeit des Cursors. Diese Methode soll allerdings so wie oben beschrieben Funktionieren: Ist der Cursor zu klein oder zu groß, wird er unsichtbar gemacht. So kann man sich eine weitere Methode sparen, die für die Cursorsichtbarkeit zuständig wäre.
Die erste Abfrage prüft nun also die als Parameter übergebene Größe. Ist diese kleiner als 1 oder größer als 100 wird die Größe des Cursors in der Informationsstruktur auf 1 gesetzt und die Sichtbarkeit auf FALSE. Ist die Größe um richtigen Wertebereit, wird die Größe in der Informationsstruktur auf die per Parameter übergebene Größe gesetzt und die Sichtbarkeit auf TRUE.
Am Ende der Methode wird die Struktur schließlich an SetConsoleCursorInfo übergeben, um die Änderungen wirksam zu machen. Die Funktion würde übrigens fehlschagen, wenn die Cursorgröße kleiner als 1 oder größer als 100 wäre, was hier ja aber garnicht passiert kann.
GetCursorSize
Diese Methode füllt eine CONSOLE_CURSOR_INFO Struktur mithilfe der Funktion GetConsoleCursorInfo. Schlägt die Funktion fehl, wird als Größe 0 zurückgegeben. Selbiges gilt, wenn der Cursor unsichtbar, das heißt Info.bVisible FALSE ist. Ansonsten wird als Cursorgröße Info.dwSize zurückgegeben.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
|
DWORD Console::GetCursorSize() const
{
CONSOLE_CURSOR_INFO Info;
if (!GetConsoleCursorInfo(Handle, &Info))
return 0;
if (!Info.bVisible)
return 0;
return Info.dwSize;
}
|
Write
So, nun folgt eine vergleichsweise große Funktion, da diese auch nicht nur eine einfache Kapselung ist. Diese Funktion erlaubt es, an einer beliebigen Stelle Text zu zeichnen, sogar mit Zeilenumbrüchen, falls erwünscht. Das beste ist allerdings, dass wenn man mit dieser Funktion ein weiteres Mal, allerdings einen anderes Text, an die gleiche Stelle schreibt, wirklich nur die Zeichen, die neu sind ersetzt werden.
Beispiel: Man schreibt denn Text "Hallo". Nun schreibt man den Text "Hello". Statt "Hallo" komplett zu löschen und "Hello" an diese Stelle zu schreiben, wird nur das "a" mit einem "e" überschrieben. Vor allem wenn man plant, ein Spiel mit einer "echten" Welt in der Konsole zu schreiben, ist dies sehr nützlich. Würde man immer alles neu zeichnen würde es sehr stark flimmern und das Programm wäre sehr langsam.
Die ersten beiden Parameter stellen die Position dar, an welcher geschrieben werden soll. Der dritte Parameter ist der Text, der geschrieben werden soll und der vierte legt die Schriftfarbe des gesamten Textes fest. Man kann hier einen oder mehrere, mit einem logischen Oder verknüpften, Werte von ConsoleColor verwenden.
Was innerhalb der Methode passiert, erkläre ich direkt per Kommentar.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
bool Console::Write(SHORT X, SHORT Y, const TCHAR* String, WORD Color)
{
DWORD Read = 0;
WORD CurrentBuffer = 0;
TCHAR TextBuffer[4000];
WORD ColorBuffer[4000];
COORD Position = {0, 0};
//Zuerst wird überprüft, ob überhaupt ein String zum Schreiben übergeben wurde
if (!String)
return false;
//Hier werden ab der per Parameter angegebenen Position die nächsten 4000 Zeichen
//gelesen und in die Variable TextBuffer übertragen. Da die Position bereits auf 0|0
//gesetzt wurde, werden eben die ersten 4000 Zeichen gelesen. Wer sich fragt, warum
//ausgerechnet die ersten 4000, dem sei gesagt, dass die Konsole im Vollbildmodus
//genau 80*50 groß ist, das heißt 4000 Zeichen in sie hineinpassen. Falls man weiter
//in die Tiefe gehen will, also über eine Y-Position von 49 hinaus, kann mit einer
//Funktion wie dieser hier sowieso nichts oder zumindest kaum etwas anfangen und
//sollte lieber std::cout aus der C++ Standardbibliothek verwenden.
//Der letzte Parameter ist ein Zeiger auf eine Variable, in welche die tatsächliche
//Anzahl der gelesenen Zeichen übertragen wird. Diese Zahl ist hier jetzt allerdings
//unwichtig. Trotzdem wird eine Adresse übergeben, da die Funktion sonst fehlschlägt.
if (!ReadConsoleOutputCharacter(Handle, TextBuffer, 4000, Position, &Read))
return false;
//Diese Funktion funktioniert genau wie die vorige, mit dem Unterschied, dass
//hier die Farben und nicht die Zeichen ausgelesen werden.
if (!ReadConsoleOutputAttribute(Handle, ColorBuffer, 4000, Position, &Read))
return false;
//Hier wird nun die Positionsstruktur mit den
//per Parameter übergebenen Werden gefüllt
Position.X = X;
Position.Y = Y;
//CurrentBuffer ist die Position eines Zeichens oder einer Farbe im TextBuffer
//bzw. ColorBuffer Array. Sie wird mithilfe der X- und Y-Position berechnet.
CurrentBuffer = Position.X + (Position.Y * 80);
//Hier wird nun jedes Zeichen des zu zeichnenden Strings durchgegangen und mit dem
//jeweiligen Zeichen des Buffers verglichen. Sind die Zeichen nicht identisch, wird
//das alte Zeichen überschrieben.
//_tcslen wird bei der Verwendung von Ansi zu strlen und bei der Verwendung von
//Unicode zu wcslen. Jede dieser Funktionen gibt die Länge einer Zeichenkette zurück.
for (size_t i = 0; i < _tcslen(String); i++)
{
//Falls das aktuelle Zeichen die Escape-Sequenz \n ist, wird ein Zeilenumbruch durchgeführt.
//Dazu wird zur Variable CurrentBuffer 80 addiert, um genau eine Zeile weiter unten zu landen.
//Allerdings soll die Finale X-Position ja die, die per Parameter übergeben wurde, sein.
//Deshalb wird erst einmal die aktuelle X-Position subtrahiert (entspricht X = 0) und dann die
//per Parameter übergebene addiert. Nun wird noch die aktuelle X-Position auf die
//Anfangs-X-Position gesetzt und die Y-Position um eins erhöht.
if (String[i] == TEXT('\n'))
{
CurrentBuffer += (80 - Position.X) + X;
Position.X = X;
Position.Y++;
}
else
{
//Falls die X-Position größer als 79 ist,
//muss ein Zeilenumbruch stattfinden.
if (Position.X > 79)
{
Position.X = X;
Position.Y++;
}
//Hier werden nun Zeichen verglichen, bei Unterscheidung
//wird an die aktuelle Position neu gezeichnet.
if (String[i] != TextBuffer[CurrentBuffer])
{
if (!FillConsoleOutputCharacter(Handle, String[i], 1, Position, &Read))
return false;
}
//Hier findet genau das gleiche statt wie
//bei den Zeichen, nur diesmal mit Farben,
if (Color != ColorBuffer[CurrentBuffer])
{
if (!FillConsoleOutputAttribute(Handle, Color, 1, Position, &Read))
return false;
}
//Hier werden Positionen aktualisiert
Position.X++;
CurrentBuffer++;
}
}
return true;
}
|
Hier sollte man sich Zeit lassen, alles zu verstehen, da es schon ein ordentlicher Klotz Code ist.
Wenn man so weit ist, geht es nun weiter.
Write (Überladung)
Write ist eine überladene Funktion. Die zweite Variante erwartet statt einer einzelnen Farbe, die für den ganzen Text gilt, eine Farbe pro Zeichen. Ansonsten gibt es keinen Unterschied zur Standard-Variante, weshalb hier auch nichts speziell erklärt werden muss.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
bool Console::Write(SHORT X, SHORT Y, const TCHAR* String, const WORD* Color)
{
DWORD Read = 0;
WORD CurrentBuffer = 0;
TCHAR TextBuffer[4000];
WORD ColorBuffer[4000];
COORD Position = {0, 0};
if (!String || !Color)
return false;
if (!ReadConsoleOutputCharacter(Handle, TextBuffer, 4000, Position, &Read))
return false;
if (!ReadConsoleOutputAttribute(Handle, ColorBuffer, 4000, Position, &Read))
return false;
Position.X = X;
Position.Y = Y;
CurrentBuffer = X + (Y * 80);
for (size_t i = 0; i < _tcslen(String); i++)
{
if (String[i] == TEXT('\n'))
{
CurrentBuffer += (80 - Position.X) + X;
Position.X = X;
Position.Y++;
}
else
{
if (Position.X > 79)
{
Position.X = X;
Position.Y++;
}
if (String[i] != TextBuffer[CurrentBuffer])
{
if (!FillConsoleOutputCharacter(Handle, String[i], 1, Position, &Read))
return false;
}
if (Color[i] != ColorBuffer[CurrentBuffer])
{
if (!FillConsoleOutputAttribute(Handle, Color[i], 1, Position, &Read))
return false;
}
Position.X++;
CurrentBuffer++;
}
}
return true;
}
|
Color
Diese Methode ähnelt stark der Write Funktion, mit dem Unterschied, dass kein Text geschrieben, sondern nur gefärbt wird. Der letzte Parameter ist die Anzahl der Zeichen, die gefärbt werden sollen.
Diese Methode sollte man ohne Probleme verstehen können, wenn man die erste Write Methode verstanden hat.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
bool Console::Color(SHORT X, SHORT Y, WORD Color, DWORD Length)
{
DWORD Read = 0;
WORD ColorBuffer[4000];
COORD Position = {0, 0};
if (!ReadConsoleOutputAttribute(Handle, ColorBuffer, Length, Position, &Read))
return false;
Position.X = X;
Position.Y = Y;
for (DWORD i = 0; i < Length; i++)
{
if (Color != ColorBuffer[i])
{
if (!FillConsoleOutputAttribute(Handle, Color, 1, Position, &Read))
return false;
}
Position.X++;
if (Position.X > 79)
{
Position.X = X;
Position.Y++;
}
}
return true;
}
|
Color (Überladung)
Hier gilt das Selbe wie bei der Überladung von Write: Es wird eine Farbe pro Zeichen erwartet.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
bool Console::Color(SHORT X, SHORT Y, const WORD* Color, DWORD Length)
{
DWORD Read = 0;
WORD ColorBuffer[4000];
COORD Position = {0, 0};
if (!Color)
return false;
if (!ReadConsoleOutputAttribute(Handle, ColorBuffer, Length, Position, &Read))
return false;
Position.X = X;
Position.Y = Y;
for (DWORD i = 0; i < Length; i++)
{
if (Color[i] != ColorBuffer[i])
{
if (!FillConsoleOutputAttribute(Handle, Color[i], 1, Position, &Read))
return false;
}
Position.X++;
if (Position.X > 79)
{
Position.X = X;
Position.Y++;
}
}
return true;
}
|
ClearScreen
Zu guter Letzt darf natürlich die Methode zum reinigen des Bildschirms nicht fehlen. Diese Methode überschreibt einfach den ganzen Bildschirm mit Leerzeichen und setzt anschließend den Cursor an die Position 0|0.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
|
bool Console::ClearScreen(void)
{
TCHAR Buffer[4000];
for (unsigned long i = 0; i < 4000; i++)
Buffer[i] = TEXT(' ');
if (!Write(0, 0, Buffer))
return false;
return SetCursorPosition(0, 0);
}
|
Das TEXT-Makro setzt übrigens in diesem Fall bei der Verwendung von Ansi das Zeichen auf ' ' und bei der Verwendung von Unicode auf L' '.
Beispielprogramm
Hier folgt nun ein kleines Beispielprogramm, welches alle Set-, Write- und Color-Methoden demonstriert. Man kann es wie einen Film auflaufen lassen, man muss also keine Eingabe tätigen.
Ich lasse das Programm unkommentiert, da es eigentlich ja nur die besprochenen Methoden aufruft.
|
C-/C++-Quelltext
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
int main()
{
unsigned short Position = 0;
unsigned short Colors[64];
Console& C = Console::Instance();
for (unsigned int i = 0; i < 64; i++)
Colors[i] = i % 2 ? CC_Red : CC_Blue;
if (!C.Init())
return 0;
Sleep(4000);
C.SetTitle(TEXT("Die Windows Konsole"));
Sleep(2000);
C.SetFullScreenState(true);
Sleep(4000);
C.SetCursorPosition(1, 1);
Sleep(4000);
C.SetCursorSize(100);
Sleep(4000);
C.Write(2, 2, TEXT("Das ist ein Test-Text\nüber mehrere Zeilen."));
Sleep(4000);
C.Write(2, 8, TEXT("Das ist ein mehrfarbiger Test-Text\nüber mehrere Zeilen."), Colors);
Sleep(4000);
C.Color(2, 2, CC_Green, 64);
Sleep(4000);
C.Color(2, 3, Colors, 64);
Sleep(4000);
C.ClearScreen();
Sleep(2000);
while (true)
{
C.Write(0, 0, TEXT("Flimmerfreies überschreiben"));
C.Write(0, 2, TEXT("#\n#\n#"));
for (unsigned short i = 0; i < 64; i++)
{
if (i == Position)
C.Write(i + 1, 2, TEXT("#\nO\n#"));
else
C.Write(i + 1, 2, TEXT("#\n.\n#"));
}
C.Write(64, 2, TEXT("#\n#\n#"));
Sleep(100);
Position++;
if (Position == 63)
{
Sleep(4000);
break;
}
}
return 0;
}
|
Links
Code
MSDN