استفاده از کنسول ویندوز راه ساده ای برای نمایش متون است. با این حال با کمی دانش فنی، به آسانی می توانید از این محیط برای نمایش تصاویر گرافیکی مبتنی بر کاراکتر های اسکی (ASCII-art graphics) و همچنین خواندن ورودی موس و صفحه کلید بهره ببرید.

 

 

ساخت پنجره های اختصاصی

اگر شما یک پروژه ی استاندارد از نوع Win32 Console Application ایجاد کرده و سپس آن را اجرا کنید، یک پنجره ی کنسول بر روی صفحه ظاهر شده و بلافاصله بسته خواهد شد. اگر شما یک نقطه توقف (breakpoint) در انتهای برنامه خود اضافه کنید تا مانع از پایان برنامه و بسته شدن پنجره شود و برنامه را دوباره اجرا کنید، پنجره ای نه چندان بزرگ و با اندازه ی پیش فرض کنسول ویندوز مشاهده خواهید کرد. شما همچنین یک میله پیمایش (scrollbar) نیز در قسمت راست این پنجره مشاهده خواهید کرد. این به این علت است که اندازه ی بافر متنی که کنسول در اختیار دارد بسیار بیشتر از اندازه کنونی پنجره ی است که در حال نمایش است.

ممکن است ما واقعا خواستار چنین چیزی نباشیم و بخواهیم پنجره ای با ابعاد – و همچنین با اندازه ی بافر - دلخواه خودمان ایجاد کنیم.
برای مثال در این بخش ما می خواهیم پنجره ای با 80 ستون و 50 سطر را مورد استفاده قرار دهیم، که اندازه ای متناسب و همچنین استاندارد به شمار می رود. زمانی که با استفاده از کلید های Alt + Enter پنجره ی کنسول را تمام صفحه می کنید نیز صفحه با همین اندازه نمایش داده می شود.

هنگامی که ما یک پنجره ی کنسول را ایجاد کردیم، برای کنترل آن نیاز به دو دستگیره یا هندل (handle) خواهیم داشت.

دستگیره ها، متغیر هایی هستند که به بخش هایی از پنجره ی کنسول اشاره می کنند (مثلا بخش وروردی و یا خروجی) و در هنگام استفاده از توابع مختلف کنسول که کار خاصی را بر روی یک پنجره کنسول انجام می دهند، این دستگیره ها به عنوان آرگومان به توابع مذکور ارسال می شوند تا به آنها بگویند که عملیات مورد نظر باید بر روی کدام پنجره کنسول انجام شود.

همانطور که اشاره شد، ما به دو دستگیره نیاز خواهیم داشت. یک دستگیره برای نوشتن در محیط کنسول – ارسال متن و دیگر اطلاعات کنترلی به آن – که آن را wHnd خواهیم نامید. و همچنین یک دستگیره هم برای خواندن از محیط کنسول – به طور مثال دسترسی به رویداد های موس و کیبورد- که آن را rHnd خواهیم نامید. متغیر های rHnd و wHnd از نوع HANDLE می باشند که یک نوع تعریف شده در سرفایل windows.h است. ما مقادیر این دو متغیر را با استفاده از مقادیری که تابع (...)GetStdHandle به ما باز می گرداند تنظیم خواهیم کرد. این تابع تنها یک پارامتر دارد و با دریافت مقادیر STD_OUTPUT_HANDLE و STD_INPUT_HANDLE به ترتیب هندل نوشتن و هندل خواندن در پنجره ی کنسول جاری را باز می گرداند.

در زیر نمونه کدی برای آنچه تا به اینجا ذکر شد، آورده شده است: 

#include <stdlib.h> 
#include <Windows.h> 
  
HANDLE wHnd;    // Handle to write to the console. 
HANDLE rHnd;    // Handle to read from the console. 
  
int main(int argc, char* argv[]) { 
  
    // Set up the handles for reading/writing: 
    wHnd = GetStdHandle(STD_OUTPUT_HANDLE); 
    rHnd = GetStdHandle(STD_INPUT_HANDLE); 
  
    // Exit 
    return 0; 
}

اگرچه این هنوز خیلی هیجان انگیز نیست، با این حال اکنون ما می توانیم با استفاده از این دستگیره ها، به کنسول دسترسی داشته باشیم و آن را کنترل کنیم.

یکی از ساده ترین چیزهایی که می توانیم در پنجره ی کنسول تغییر دهیم، متنی است که در نوار عنوان قرار دارد. این کار به قدری ساده است که برای انجام آن حتی لازم نیست که هیچ دستگیره ای را به تابعی ارسال کنیم. تنها کافیست خط زیر را به برنامه ی خودمان اضافه کنیم.

SetConsoleTitle(L"Win32 Console Control Demo");

 اکنون عنوان پنجره را به دلخواه خودمان تنظیم کرده ایم. با این حال این پنجره هنوز هم اندازه ی مورد نظر ما را ندارد. خوشبختانه تابع (...)SetConsoleWindowInfo می تواند به کمک ما بیاید. این تابع سه آرگومان می گیرد:
دستگیره نوشتن در پنجره ی کنسول
یک پرچم (flag) از نوع داده ی boolean که مشخص می کند ما قصد تنظیم سایز به طور مطلق را داریم و یا اینکه می خواهیم آنرا به صورت نسبتی از سایز پنجره ی فعلی تنظیم کنیم
یک اشاره گر به یک متغیر از ساختار SMALL_RECT که حاوی اطلاعات مربوط به سایز جدید می باشد
ساختار SMALL_RECT از چهار متغیر short integer تشکیل شده است که به ترتیب نمایانگر گوشه های سمت چپ، بالا، راست و پایین یک چهار گوش می باشند.
با توجه به اینکه ما می خواهیم اندازه ی پنجره ی کنسول را از نو تنظیم کنیم، از مقادیر مطلق برای اندازه پنجره استفاده خواهیم کرد. همچنین با توجه اینکه می خواهیم اندازه ی پنجره ی ما 80x50 باشد باید از محدوده مختصات (0,0) تا (79,49) استفاده کنیم.
هنگامی که یک متغیر از نوع SMALL_RECT ساخته شد، می توانیم به صورت مجزا مقادیر Left, .Top, .Right and .Bottom. را تنظیم کنیم. همچنین می توانیم با مقدار دهی اولیه و از دستور {Left, .Top, .Right, .Bottom.} - که در مثال زیر نیز مورد استفاده قرار گرفته است - استفاده کنیم:

#include <stdlib.h> 
#include <Windows.h> 
  
HANDLE wHnd;    // Handle to write to the console. 
HANDLE rHnd;    // Handle to read from the console. 
  
int main(int argc, char* argv[]) { 
  
    // Set up the handles for reading/writing: 
    wHnd = GetStdHandle(STD_OUTPUT_HANDLE); 
    rHnd = GetStdHandle(STD_INPUT_HANDLE); 
  
    // Change the window title: 
    SetConsoleTitle(L"Win32 Console Control Demo"); 
  
    // Set up the required window size: 
    SMALL_RECT windowSize = {0, 0, 79, 49}; 
      
    // Change the console window size: 
    SetConsoleWindowInfo(wHnd, TRUE, &windowSize); 
  
    // Exit 
    return 0; 
}

 خب، تا اینجا همه چیز درست و خوب به نظر می رسد. اما اگر برنامه را اجرا کنید متوجه یک ایراد آشکار خواهید شد. با اینکه پنجره ی ما ابعاد درستی دارد (برای اطمینان بر روی نوار عنوان کلیک راست کرده و Properties را انتخاب کنید) شما متوجه خواهید شد که هنوز هم نوار پیمایش وجود دارد و هنوز هم 300 خط در پنجره ی کنسول به صورت داخلی قابل پیمایش است. ما نیاز داریم تا این مورد را نیز رفع کنیم.

در اینجا یک تابع دیگر به نام (...)SetConsoleScreenBufferSize وجود دارد که می تواند به حل مشکل کمک کند.
این تابع دو پارامتر دریافت می کند:
دستگیره ی نوشتن در کنسول
یک متغیر از ساختار COORD که حاوی سایز بافر مورد نظر می باشد
ساختار COORD از دو متغیر short integer به نام های X و Y تشکیل شده است. در این مورد X برای تنظیم عرض (width) بافر و Y برای تنظیم ارتفاع (height) بافر کنسول، مورد استفاده قرار می گیرند.
همانند SMALL_RECT، برای تنظیم مقادیر X و Y در ساختار COORD می توانیم با مقدار دهی اولیه و از دستور {X, Y} استفاده کنیم.
برای تنظیم بافر کنسول در برنامه خود این خطوط را اضافه می کنیم:

// Create a COORD to hold the buffer size: 
COORD bufferSize = {80, 50}; 
 
// Change the internal buffer size: 
SetConsoleScreenBufferSize(wHnd, bufferSize);

 برنامه را اجرا کنید، به نظر می رسد این بار، مقادیر به طور مناسب تنظیم شده اند و برنامه به درستی کار می کند.

نوشتن در کنسول
به طور معمول برای نوشتن یک متن ساده در کنسول، ما می توانیم از توابعی نظیر (...)printfاستفاده کنیم. اما این توابع فاقد کنترل لازم بر روی متن هستند که ما به آن نیاز داریم. برای اینکه کنترل مناسبی بر روی متن ها در کنسول داشته باشیم نیاز به استفاده از توابع و کدهای کمی سطح پایین تر داریم.

هر کاراکتر در کنسول به وسیله ی ساختاری به نام CHAR_INFO نشان داده می شود. این ساختار، هم حاوی کاراکتری است که ما می خواهیم نمایش داده شود و هم رنگی که می خواهیم کاراکتر با آن نمایش داده شود. اطلاعات مربوط به رنگ کاراکتر در خاصیتی به نام Attributes نگهداری می شوند که خود تشکیل شده از رنگ پس زمینه و رنگ پیش زمینه (و مقداری اطلاعات دیگر از قبیل خصوصیات حاشیه) و اطلاعات کاراکتر نیز در کاراکترهای اسکی در خاصیتی به نام Char.AsciiChar و در کاراکترهای یونیکد در خاصیتی به نام Char.UnicodeChar نگهداری می شوند.



اجازه بدهید تا بحث را با نحوه ی ایجاد کاراکتری که در بالا مشاهده می کنید ادامه بدهیم. کاملا واضح است که این کاراکتر اسکی 'A' است (دقت کنید کنید که ما از شکل "A" – که یک رشته است - برای نمایش آن استفاده نکردیم. یک رشته، آرایه ای از کاراکتر ها است که با کاراکتر '0\' پایان می یابد و آن چیزی نیست که مد نظر ما است. به همین دلیل از 'A' استفاده کردیم تا مشخص کنیم که منظور ما یک کاراکتر است).
علاوه بر اینکه ما از 'A' برای نمایش کاراکتر A استفاده می کنیم، می توانیم از کد های انسی (ANSI) استفاده کنیم.

در زیر لیستی از این کاراکتر ها برای کد گذاری پیش فرض (850) مشاهده می کنید.


همه اعداد ذکر شده در بالا (در سمت چپ هر کاراکتر) به فرم هگزادسیمال هستند. بنابر این ما می توانیم کاراکتر 'A' را به وسیله ی دستور زیر مشخص کنیم:

Char.AsciiChar = 0x41;

 برای ساخت رنگ پیش زمینه، ما به این ثوابت از پیش تعریف شده، دسترسی داریم.

FOREGROUND_BLUE
FOREGROUND_GREEN
FOREGROUND_RED
FOREGROUND_INTENSITY


و برای تنظیم رنگ پس زمینه کافی است به جای _FOREGROUND در عبارات بالا از _BACKGROUND استفاده کنیم.
در اینجا رنگ پیش زمینه ی ما یک قرمز روشن است. بنابر این ما از ترکیب FOREGROUND_RED|FOREGROUND_INTENSITY استفاده خواهیم کرد. با این حال طرز ساخت رنگ پس زمینه کمی پیچیده تر خواهد بود. برای ایجاد رنگ زرد، ما نیاز به ترکیب رنگ های قرمز و سبز داریم . در نهایت خاصیت Attribute باید به شکل زیر تنظیم شود:
FOREGROUND_RED|FOREGROUND_INTENSITY|BACKGROUND_RED |BACKGROUND_GREEN|BACKGROUND_INTENSITY

تابعی که نیاز داریم تا به وسیله ی آن کاراکتر ها را در خروجی کنسول بنویسیم، (...)WriteConsoleOutput نام دارد. این تابع نسبتا چاق، نیاز به کمی تنظیمات دارد که ما باید آنها را در قالب ارسال پارامتر ها انجام دهیم. پارامتر های آن عبارتند از:
دستگیره ی نوشتن در کنسول
یک اشاره گر به بافر کاراکتری (CHAR_INFO) که می خواهیم نمایش داده شود
عرض و ارتفاع بافر مورد نظر
مختصات مکان شروع نوشتن در کنسول
یک اشاره گر به یک متغیر از نوع SMALL_RECT که ناحیه مورد نظر ما برای نوشتن را تعیین می کند
برای این مورد، ما فقط می خواهیم یک کاراکتر را چاپ کنیم، بنابر این اندازه بافر کاراکتر ما {1,1}، مختصات مکان شروع نوشتن {0,0} و ناحیه مورد نظر ما برای نوشتن (متغیر نوع SMALL_RECT) نیز {0,0,0,0} خواهد بود.
در زیر یک مثال کامل آورده شده است:

#include <stdlib.h> 
#include <Windows.h> 
#include <stdio.h> 
  
HANDLE wHnd;    // Handle to write to the console. 
HANDLE rHnd;    // Handle to read from the console. 
  
int main(int argc, char* argv[]) { 
  
    // Set up the handles for reading/writing: 
    wHnd = GetStdHandle(STD_OUTPUT_HANDLE); 
    rHnd = GetStdHandle(STD_INPUT_HANDLE); 
  
    // Change the window title: 
    SetConsoleTitle(L"Win32 Console Control Demo"); 
  
    // Set up the required window size: 
    SMALL_RECT windowSize = {0, 0, 79, 49}; 
      
    // Change the console window size: 
    SetConsoleWindowInfo(wHnd, TRUE, &windowSize); 
  
      
    // Create a COORD to hold the buffer size: 
    COORD bufferSize = {80, 50}; 
  
    // Change the internal buffer size: 
    SetConsoleScreenBufferSize(wHnd, bufferSize); 
  
    // Set up the character: 
    CHAR_INFO letterA; 
    letterA.Char.AsciiChar = 'A'; 
    letterA.Attributes =  
        FOREGROUND_RED 
        |FOREGROUND_INTENSITY 
        |BACKGROUND_RED 
        |BACKGROUND_GREEN 
        |BACKGROUND_INTENSITY; 
  
    // Set up the positions: 
    COORD charBufSize = {1,1}; 
    COORD characterPos = {0,0}; 
    SMALL_RECT writeArea = {0,0,0,0};  
  
    // Write the character: 
    WriteConsoleOutput(wHnd, &letterA, charBufSize, characterPos, &writeArea); 
  
    // Exit 
    return 0; 
}

 نکته: هدر فایل stdio.h برای استفاده از تابع (...)printf به صورت اختیاری آورده شده است.

 در هنگام اجرا، شما باید چیزی شبیه به تصویر بالا را مشاهده کنید. به نظر می رسد فقط برای چاپ یک کاراکتر بیش از اندازه مشکل بود، اینطور نیست ؟

double-buffering
اگر شما برنامه های گرافیکی می نویسید، خواهید دانست که به طور معمول بهتر است که تمام ترسیم ها را در یک بافر خارج از صفحه نمایش (off-buffer or back-buffer) انجام دهیم و سپس آنها را به یک باره در صفحه نمایش کپی کنیم. این کار پرش هایی (flickers) که در هنگام ایجاد یک تصاویر شلوغ (تصاویری حاصل از ترسیم های زیاد) به وجود می آیند را حذف خواهد کرد.

ما می توانیم به سادگی از سیستم double-buffering برای نمایش تمام صفحه ی متن و اطلاعات رنگی به صورت آنی در محیط کنسول استفاده کنیم.
طبیعتا، ما تغییرات مورد نظر بر روی یک CHAR_INFO را درون آرایه ای از CHAR_INFO هایی که قرار است آنها را در صفحه نمایش کپی کنیم، انجام می دهیم.

به دلیل اینکه ما از بافر کنسول با ابعاد 80x50 استفاده می کنیم، آرایه مورد استفاده برای بافر پشتی نیز باید 4000 عنصر داشته باشد. علاوه بر این ما باید charBufSize (برای معین کردن عرض و ارتفاع بافر ارسالی به تابع (...)WriteConsoleOutput) را به ابعاد 80x50 تنظیم کنیم و همچنین ناحیه مورد نظر برای نوشتن (writeArea) را طوری تغییر دهیم که کل صفحه را در بر بگیرد. در نهایت کدی احتیاج داریم به این شکل است:

// Set up the character buffer: 
CHAR_INFO consoleBuffer[80*50]; 
 
// We'll fill the console buffer with random data: 
for (int y=0; y<50; ++y) { 
    for (int x=0; x<80; ++x) { 
      
        // An ASCII character is in the range 0-255, 
        // so use % to keep it in this range. 
        consoleBuffer[x+80*y].Char.AsciiChar = rand()%256; 
 
        // The colour is also in the range 0-255, 
        // as it is a pair of 4-bit colours. 
        consoleBuffer[x+80*y].Attributes = rand()%256; 
    } 
}

 امیدوارم متوجه شده باشید که کدهای جدید از کجا آمده اند. ما مجبوریم که از یک آرایه خطی به جای یک آرایه دو بعدی برای بافر پشتی استفاده کنیم. به همین علت از این فرمول استفاده کردیم:

array_index = x + array_width * y

برای تغییر مختصات، ما ایندکس معادل را در آرایه ی تک بعدی پیدا کرده و در آن می نویسیم.

حال با اینکه این اطلاعات به خودی خود مفید هستند، ایده ی خوبی خواهد بود که ببینیم چطور می توانیم ورودی را از کاربر بگیریم تا بتوانیم از دانش خود بهترین استفاده را ببریم.


منبع: برنامه نویسی کنسولی