مسح محتويات الـ Console برمجيا باستخدام دوال الويندوز API

This article is available in English too, check it out here.

الكود: Geming.Suite.Console.zip

المحتويات

محتويات هذا الدرس:

  • المحتويات
  • نظرة خاطفة
  • مقدمة
  • الدوت نت والـ API
  • 1. تجهيز الـ Console للعمل
  • 2. معرفة مساحة الـ Console
  • 3. المسح
  • 4. تحريك المؤشر
  • التطبيق
  • الكود
  • خاتمة

نظرة خاطفة

الدرس اليوم هو أحد الدروس الأخرى المتعلقة بكيفية برمجة دوال الويندوز Windows API من خلال بيئة الدوت نت.

الدرس يتكلم عن كيفية مسح محتويات الشاشة في مشاريع الـ Console باستخدام دوال الويندوز عن طريق الـ C#. حتى لو كانت هذه الإمكانية متوفرة في بيئة الدوت نت، فهذا الدرس يساعدك على فهم أعمق لكيفية برمجة دوال الويندوز API من خلال بيئة الدوت نت وكيفية التعامل مع الذاكرة وغيرها من الأفكار التي سنتعرض لها. مع ذلك، فبيئة الدوت نت لا توفر لك إمكانية لمسح جزء معين من الشاشة وهذا ما سنتعلمه أيضا في هذا الدرس.

مع الدرس، مشروع صغير بالـ C# عبارة عن مكتبة توضح كيفية الاستفادة من بعض إمكانيات الـ Console المتوفرة من خلال الـ API وليست متوفرة من خلال الدوت نت مثل كيفية تحريك النصوص داخل الشاشة وكيفية إخفاء وإظهار المؤشر ونحوها.

مقدمة

أعتبر نفسي من كبير المشجعين لمشاريع الـ Console عندما تحتاج إلى تطوير أداة أو برنامج بسيط (من حيث التصميم) يقوم بتنفيذ مهام معينة (مهما كان تعقيدها أو صعوبتها) لن تحتاج إلى الكثير من التفاعل مع المستخدم.

وأحد الأوامر الهامة التي أحيانا تجب على المستخدم ألا وهي مسح محتويات الشاشة أو جزأ معين منها. وقبل الإصدار 2.0 من بيئة الدوت نت لم تكن هذه الإمكانية متوفرة وكان على المبرمج أن يقوم باستخدام الـ API لتحقيق مبتغاه. مع ذلك، حتى بعد الإصدار 2.0 والذي وفر لنا إمكانية لمسح محتويات الشاشة جميعا، تتبقى إمكانية مسح جزء معين من الشاشة غير متاحة. وهذا ما سنتكلم عنه اليوم، وهو مسح جزء معين من الشاشة أو مسح جميع الشاشة برمجيا باستخدام الـ API.

يمكننا تنفيذ عملية المسح (سواء لجميع الشاشة أو جزء منها) عن طريق اتباع خطوات بسيطة وهي -على الترتيب-:

  1. تجهيز الـ Console للعمل:
    تجهيزه لعملية الإخراج Output، حيث أن تعديلات على محتوى الشاشة تدخل في هذا النوع.
  2. الحصول على الأبعاد وقياسات الـ Console:
    لكي نستطيع مسح محتوياته، يجب علينا معرفة المساحة الخاصة به كي لا نخرج عنها.
  3. المسح:
    ويتم ذلك عن طريق محتويات الجزء المطلوب مسحه بمسافات.
  4. إعادة المؤشر:
    إعادة مؤشر الكتابة إلى وضعه الأول. فإن كنت مسحت جميع الشاشة فيجب عليك إعادته إلى موقعه الأصلي في أعلى يسار الشاشة.

لاحظ أن جميع الدوال Functions والتركيبات Structures التي سنستخدمها موجودة في مكتبة الويندوز kernel32.dll.

وقبل أن نبدأ في الخطوات سنتعرف على اثنان من أهم المفاهيم التي نحتاجها في هذا الدرس وهما Platform Invocation و Marshaling.

الدالة System.Console.Clear() في بيئة الدوت نت تقوم بتنفيذ هذه الخطوات داخليا.

الدوت نت والـ API

كما تعلم، ليس هناك أي توافقية Compatibility بين بيئة الدوت نت (أو كما تسمى الكود المدار Managed Code) وبين الـ Windows API (يسمى الكود الغير مدار Unmanaged Code.) فلهذا، لن يمكنك التعامل مع الدوال والتركيبات Structures الخاصة بالـ API مباشرة، ولكن ستحتاج إلى إجراء عمليتين وهما Platform Invocation و Marshaling.

Platform Invocation أو PInvoke (لاحظ أن الـ C تحولت إلى K في الاختصار) هي عملية النداء على أحد دوال الـ Windows API من خلال الدوت نت. ويتم ذلك عن طريق تعريف هذه الدالة بتعريف خاص بلغة الدوت نت التي تستخدمها (مثلا C#) تعريفا مشابها لتعريفات الدوال العادية ولكنه يضم بيانات إضافية مثل اسم المكتبة Library التي تحوي هذه الدالة وأيضا بعض المعدلات Modifiers الخاصة بالتعريف.

أما العملية الثانية وهي Marshaling فهي مشابهة كثيرا للأولى ولكن الفرق أن الأولى خاصة بالدوال أما هذه فخاصة بأنواع البيانات Data Types. فمثلا في الـ Windows API تجد النوع DWORD وهو غير موجود في بيئة الدوت نت. فما الحل؟

الحل أن تقوم بعملية الـ Marshaling وهي عملية تحويل هذا النوع إلى مقابله أو مثيله في بيئة الدوت نت. فالنوع DWORD مثلا عبارة عن 32-bit Unsigned Integer أي متغير رقمي حجمه 32 بت لا يسمح بالأرقام السالبة (تحول إلى موجبة بطريقة معينة.) فالنوع DWORD يقابله في بيئة الدوت نت النوع System.UInt32 (أي uint في C#.) إذا فالنوع System.UInt32 هو الـ Marshaling Type للنوع DWORD.

مع مقدمة بسيطة، هيا بنا إلى الخطوات.

من الهام معرفة أن فكرة تعامل الدوت نت مع تقنيات أخرى بما تحويه من جميع العمليات تسمى Interoperability.

1. تجهيز الـ Console للعمل

ماذا تريد؟ كيف تريد العمل مع الـ Console؟ هل تريد استقبال بيانات يدخلها المستخدم مثلا؟ أم تريد تغيير محتويات الشاشة؟ إذا كان الأخير ما تريده فأنت تريد تجهيزه لعملية الإخراج Output وهذا ما سنفعله الآن.

أيا كان ما تريد عمله على الـ Console له فأنت تحتاج إلى تجهيزه للعمل، أو بالأصح الحصول على مقبض هذا الـ Console، أي العنصر الذي من خلاله تستطيع التحكم به. فإنت كنت تريد القراءة فأنت تريد أن تحصل على مقبض للقراءة Input Handle، أو الكتابة فمقبض للكتابة Output Handle. وسواء ما تريده فأنت تريد استخدام دالة الويندوز GetStdHandle() المسؤولة عن الحصول على مقابض الـ Console، والتي لها التركيب التالي (في C):

HANDLE GetStdHandle(
  DWORD nStdHandle
);

هذه الدالة تأخذ مدخلا واحدا وهو نوع المقبض وترجع هذا المقبض المطلوب.

ويمكنك تحديد نوع المدخل عن طريق استخدام أحد الثوابت التالية:

  • STD_INPUT_HANDLE = -10:
    للحصول على مقبض الإدخال Input Handle (هذا نستخدمه في دروس أخرى بإذن الله تعالى.)
  • STD_OUTPUT_HANDLE = -11:
    للحصول على مقبض الإخراج Output Handle (وهذا ما نحتاج إليه اليوم.)
  • STD_ERROR_HANDLE = -12:
    للحصول على مقبض لإخراج الأخطاء Errors (دعك منه الآن.)

كما عملت، لن يمكنك النداء على هذه الدالة مباشرة من خلال الدوت نت، ولكن يجب عليك تعريفها تعريفا خاصا بالدوت نت أي PInvoke. وهذا التعريف هو كالتالي:

[System.Runtime.InteropServices.DllImport("Kernel32.dll")]
static extern IntPtr GetStdHandle(uint nStdHandle);

أيضا، فهذه الثوابت Constants غير متوفرة في بيئة الدوت نت، فيمكنك تعريفها أيضا كما تعرف الثوابت دائما.

const int STD_OUTPUT_HANDLE = -11;

لن نخوض في شرح هذا التعريف حيث أننا سنتكلم عنه بشكل مفصل في دروس أخرى بإذن الله تعالى. ولكن حتى هذا الوقت، يمكنك استخدام هذا التعريف كما هو بدون أي تغيير (فقط يمكنك إضافة المعدل public إلى بدايته.)

ملاحظة: معظم الـ Attributes الخاصة بعمليات الـ Interoperation (العمليات الخاصة بالتفاعل بين الدوت نت والتقنيات الأخرى) موجودة في الـ Namespace المسمى System.Runtime.InteropServices ولذلك يفضل إضافته إلى قائمة الـ using الخاصة بك.

2. معرفة مساحة الـ Console

قبل أن نعرف كيفية الحصول على الأبعاد للـ Console برمجيا يجب علينا أولا دراسة شاشة الـ Console لنعرف كيف تتكون وما هي هذه الأبعاد التي نريدها.

الشكل التالي، شكل 1 يوضح شاشة الـ Console وتركيبها.

شكل 1 - مكونات شاشة الـ Console

نلاحظ في هذا الشكل أنه هناك نوعين من الأبعاد:

  • أبعاد النافذة Window Dimensions:
    وهي العرض والطول للنافذة بالبيكسل.
  • مساحة التخزين Buffer Length:
    وهي المساحة (الطول × العرض) للمكان الذي يسمح لك بالكتابة فيه في الـ Console (عدد الأحرف.)

وهنا لاحظ الفرق، حجم النافذة ربما يكون صغيرا. ولكن هناك أشرطة تمرير Scroll Bars تمكنك من مشاهدة مساحة التخزين Buffer الذي يمكنك الكتابة فيه بالكامل.

إذا فيمكنك تغيير حجم النافذة عن طريق استخدام من مقبض التحجيم Size Grip (من أقصى اليمين تحت) أو عن طريق جذب أحد أذرع النافذة، أو عن طريق تكبيرها Maximize.

أما مكان التخزين Buffer فيمكنك تغيير حجمه عن طريق خصائص شاشة الـ Console أو برمجيا عن طريق أوامر معينة (ربما نتعرض لها في دروس قادمة بإذن الله تعالى.)

لاحظ أيضا أنه لدينا مكان للمؤشر وهو موقعه في الشكل السابق شكل 1 في السطر الرابع (له الرمز 3) العمود رقم 28 (له الكود 27.)

بعد أن تعرفت على مكونات شاشة الـ Console الآن نعرف كيف نحصل على هذه المعلومات. يتم ذلك عن طريق الدالة GetConsoleScreenBufferInfo() والتي لها التعريف التالي:

BOOL GetConsoleScreenBufferInfo(
  HANDLE hConsoleOutput,
  [out] SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo
);

هذه الدالة تأخذ مدخلات مقبض إخراج Output للـ Console وترجع البيانات المطلوبة في المدخل الثاني وهو من نوع SCREEN_BUFFER_INFO وهو له التركيب التالي:

typedef struct {
  COORD dwSize;
  COORD dwCursorPosition;
  WORD wAttributes;
  SMALL_RECT srWindow;
  COORD dwMaximumWindowSize;
} CONSOLE_SCREEN_BUFFER_INFO;
typedef struct {
  SHORT X;
  SHORT Y;
} COORD;
typedef struct {
  SHORT Left;
  SHORT Top;
  SHORT Right;
  SHORT Bottom;
} SMALL_RECT;

هذا التركيب يتكون من 5 أعضاء وهم على الترتيب:

  • dwSize:
    مساحة التخزين Buffer وهي عبارة عن تركيب آخر من نوع COORD يحوي فقط عنصرين X و Y وهما الأبعاد.
  • dwCursorPosition:
    مكان مؤشر الكتابة وهو أيضا من نوع COORD يحوي الموقع.
  • wAttributes:
    خصائص شاشة الـ Console (لن نحتاج إليها في هذا الدرس، ولن نتعرض لها الآن حيث أنها تحتاج إلى درس بالكامل.)
  • srWindow:
    عنصر من نوع SMALL_RECT يحوي موقع النافذة Location وأبعادها.
  • dwMaximumWindowSize:
    أقصى حجم ممكن أن تصل إليه النافذة. لاحظ أنك عند عمل تكبير Maximize للنافذة فإنها لا تملأ الشاشة بالكامل ولا تستطيع تكبيرها أكبر من هذا. وهذا الحجم هو ما يحويه هذا العنصر.

بالطبع فهذا تعريف هذه الدالة (عملية PInvoke) وهذه التركيبات Structures (عملية Marshaling) في بيئة الدوت نت:

لاحظ أنك تحتاج إلى إضافة الـ Namespace المسمى System.Runtime.InteropServices إلى قائمة الـ using الخاصة بك في هذا المثال والأمثلة القادمة.

[DllImport("Kernel32.dll")]
static extern int GetConsoleScreenBufferInfo
    (IntPtr hConsoleOutput,
    out CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);
[StructLayout(LayoutKind.Sequential)]
struct CONSOLE_SCREEN_BUFFER_INFO
{
    public COORD dwSize;
    public COORD dwCursorPosition;
    public ushort wAttributes;
    public SMALL_RECT srWindow;
    public COORD dwMaximumWindowSize;
}
[StructLayout(LayoutKind.Sequential)]
struct COORD
{
    public short X;
    public short Y;
}
[StructLayout(LayoutKind.Sequential)]
struct SMALL_RECT
{
    public short Left;
    public short Top;
    public short Right;
    public short Bottom;
}

مرة أخرى، دعك من كيفية عمل الـ Marshaling لهذه التركيبات للآن، فقط خذها كما هي. في دروس آخرى سنتعرف على كيفية عمل هذه التركيبات بالكامل.

3. المسح

حصلت على مقبض التحكم في الـ Console، وحصلت على أبعاد مساحة التخزين Buffer وقمت بتحديد المساحة المعينة في هذه الـ Buffer الذي تريد مسحها، ماذا بعد؟ الآن يمكنك إجراء عملية المسح.

كما تلاحظ فإن مساحة التخزين Buffer له مساحة محدودة، وعند تعديها يقوم الـ Console تلقائيا بإزالة بعض البيانات القديمة لإضافة البيانات الجديدة.

إذا فكيف تتم عملية المسح؟ يتم ذلك عن طريق تبديل المحتوى المطلوب مسحه بالمسافات Spaces المعروفة وبالتالي تتم عملية المسح!

ولتبديل جزء معين من الشاشة بحروف معينة يمكننا استخدام الدالة FillConsoleOutputCharacter() والتي لها التعريف التالي:

BOOL FillConsoleOutputCharacter(
  HANDLE hConsoleOutput,
  TCHAR cCharacter,
  DWORD nLength,
  COORD dwWriteCoord,
  [out] LPDWORD lpNumberOfCharsWritten
);

تأخذ هذه الدالة 5 مدخلات:

  • hConsoleOutput:
    مقبض الإخراج للـ Console.
  • cCharacter:
    الحرف الذي تريد استخدامه (وهو المسافة في حالتنا هذه.)
  • nLength:
    المساحة التي تريد تبديلها.
  • dwWriteCoord:
    عنصر من نوع COORD يحدد المكان الذي تريد البدأ بالتبديل منه. والمكان يكون بالأعمدة والصفوف وهي قائمة على الرقم 0 كبداية لها. فمثلا السطر الأول العمود الأول له القيمة 0، 0. والسطر الثاني العمود الأول له القيمة 1، 0.
  • lpNumberOfCharsWritten:
    مدخل يستخدم لإرجاع عدد الحروف التي تم تبديلها.

والآن فهذه تعريف دالة FillConsoleOutputCharacter() تعريف خاص بالدوت نت أي PInvoke Method (لاحظ أن تعريف التركيب COORD تم ذكره سابقا.)

[DllImport("Kernel32.dll")]
static extern int FillConsoleOutputCharacter
    (IntPtr hConsoleOutput, char cCharacter, uint nLength,
    COORD dwWriteCoord, out uint lpNumberOfCharsWritten);

4. تحريك المؤشر

بعد المسح، يجب عليك إرجاع المؤشر إلى المكان الذي كان فيه. فمثلا إن مسحت جميع الشاشة يجب عليك إرجاعه إلى النقطة أقصى اليسار فوق والتي لها الإحداثيات 0، 0. ويتم ذلك عن طريق استخدام الدالة SetConsoleCursorPosition():

BOOL SetConsoleCursorPosition(
  HANDLE hConsoleOutput,
  COORD dwCursorPosition
);

ليس غريبا عليك فهذه الدالة لها مدخلان الأول هو مقبض الـ Console والثاني هو الإحداثيات.

ببساطة نقوم بعمل الـ PInvoke Method في C# كالتالي:

[DllImport("Kernel32.dll")]
static extern int SetConsoleCursorPosition
    (IntPtr hConsoleOutput, COORD dwCursorPosition);

التطبيق

تعرضنا لجميع الدوال التي سنحتاج إليها؟ الآن نعرف كيفية خلط هذه الدوال جميعها في إناء واحد.

نفترض أنك تريد مسح جميع محتويات الشاشة، كيف تفعل ذلك؟

يمكنك مسح جميع محتويات الشاشة عن طريق اتباع الخطوات التالية:

  1. الحصول على مقبض للإخراج باستخدام الدالة GetStdHandle() وباستخدام المدخل STD_OUTPUT_HANDLE.
  2. معرفة مساحة مكان التخزين Buffer عن طريق استخدام الدالة GetConsoleScreenBufferInfo()، وباستخدام العنصر dwSize الذي يحوي مساحة مكان التخزين الموجود في التركيب CONSOLE_SCREEN_BUFFER_INFO الذي ترجعه الدالة، يمكننا ضرب الطول × العرض لنحصل على مساحة مكان التخزين (عدد الأحرف التي يمكن استيعابها بالكامل.)
  3. استخدام الدالة FillConsoleOutputCharacter لتبديل مساحة التخزين هذه بمسافات وبالتالي تتم عملية المسح. ويجب التأكد من أن المسح يبدأ من النقطة ذات الإحداثيات 0، 0 ويأخذ المساحة (عدد الأحرف) كلها.
  4. تحريك المؤشر إلى النقطة 0، 0 ليصبح المستخدم قادرا على الكتابة من أول نقطة في الشاشة.

ماذا إذا كنت تريد مسح جزء معين من الشاشة؟ في الخطوة الثانية، قم بتحديد المساحة التي تريدها طبعا بعدد الأحرف، ثم قم في الخطوة الثالثة بتحديد مكان بدء المسح طبعا بالصف والعمود كما تعلم.

والآن إلى الكود.

بعد كتابتك لتعاريف الدوال PInvoke Methods والـ Marshaling Types للتركيبات Structures يمكنك كتابة الكود الذي يقوم بمسح الشاشة كاملة وهو كالتالي:

const char WHITE_SPACE = ' ';
static void Main()
{
    Console.WriteLine("Writing some text to clear...");
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine("The quick brown fox jumps over the lazy dog.");
    }
    Console.WriteLine("Press any key to clear . . . ");
    Console.ReadKey(true);
    ClearConsoleScreen();
}
static void ClearConsoleScreen()
{
    // Retrieving Console output device handle
    IntPtr handle = GetStdHandle(STD_OUTPUT_HANDLE);
    // Retrieving Console buffer information
    CONSOLE_SCREEN_BUFFER_INFO info;
    GetConsoleScreenBufferInfo(handle, out info);
    // Location of which to start filling
    COORD location = new COORD();
    location.X = 0;
    location.Y = 0;
    // Number of written characters
    uint numChars;
    FillConsoleOutputCharacter(handle, WHITE_SPACE,
        (uint)(info.dwSize.X * info.dwSize.Y),
        location, out numChars);
    // The new cursor location
    COORD cursorLocation = new COORD();
    cursorLocation.X = 0;
    cursorLocation.Y = 0;
    SetConsoleCursorPosition(handle, cursorLocation);
}

وهذا كود آخر يقوم بمسح جزء معين من الشاشة. هذا الكود يطلب من المستخدم إدخال كلمة مروره وعند فشله في الإدخال يقوم بمسح مدخلاته ويطلبها منه مرة أخرى.

const char WHITE_SPACE = ' ';
static void Main()
{
    AuthenticateUser();
}
static void AuthenticateUser()
{
    Console.WriteLine("Please enter your password:");
    Console.Write("> "); // Two characters right
    // new cursor pos: (1, 2)
    // reading user input
    string input = Console.ReadLine();
    while (input != "password")
    {
        // set the position of which to start clearing
        COORD location = new COORD();
        location.X = 2; // The third character
        location.Y = 1; // The second line
        ClearConsoleScreen(location);
        // failed, read again
        input = Console.ReadLine();
    }
    // User authenticated
    Console.WriteLine("Authenticated!");
}
static void ClearConsoleScreen(COORD location)
{
    // Retrieving Console output device handle
    IntPtr handle = GetStdHandle(STD_OUTPUT_HANDLE);
    // Retrieving Console screen buffer info
    CONSOLE_SCREEN_BUFFER_INFO info;
    GetConsoleScreenBufferInfo(handle, out info);
    // Number of written characters
    uint numChars;
    FillConsoleOutputCharacter(handle, WHITE_SPACE,
        (uint)(info.dwSize.X * info.dwSize.Y),
        location, out numChars);
    SetConsoleCursorPosition(handle, location);
}

الكود

يمكنك تحميل مكتبتنا Geming.Suite.Console والتي تحوي العديد من الأوامر الخاصة بالـ Console والتي يمكن تنفيذها من خلال الـ Windows API. من هذه الأوامر، أوامر تحريك الكلام على الشاشة، والألوان، وتغيير حجم الشاشة وحجم مكان التخزين وغيرها.

تحميل Geming.Suite.Console.zip

خاتمة

لم يكن ما تعلمناه في هذا الدرس هو كيفية مسح شاشة الـ Console فقط، بل تعلمنا أيضا كيفية النداء على دوال الويندوز من خلال الـ C# وتعلمنا العديد من الأفكار التي يمكننا تنفيذها في برامجنا.

فالآن يمكنك مثلا تحريك مؤشر الكتابة للمكان الذي تريده ومسح أجزاء من الشاشة وغيرها من المزيد من التفاعل بين البرنامج والمستخدم. والمثال يوضح العديد من الأفكار الأخرى التي يمكن تنفيذها مثل تحريك الكلام في الشاشة وتغيير الألوان وغيرها.

وفقكم الله!

مواضيع مشابهة:

اخترنا لك:

أحدث المواضيع:

هل أعجبتك؟ شارك بها...