مغامرات في دلفي

خالد الشقروني   18 06 2016

يكثر في برامجي استخدامي لدوال تغيير البيانات من نوع لآخر، و خاصة دالتي IntToStr و StrToInt ، ومع كثرة استخدامها و تنوع البيانات المراد تحويلها، تصير الأمور مزعجة بالنظر إلى طول أسماء هذه الأدوات وحرصي على كتابتها بالشكل الملائم بمراعاة الأحرف العالية والمنخفضة.

صحيح أن أصابعي أمست معتادة على طباعة أحرف بعض الإجرائيات و بسرعة كبيرة، لكن يظل الأمر مزعجا خاصة في الإجرائيات التي تتطلب أكثر من مُعطى واحد Arguments أو التي لديها أكثر من توأم overload، وما يتبعها من مراجعة المساعدة للبحث عن الإسم الصحيح للإجرائية المناسبة. ويزداد الأمر ارباكا إذا استخدمت ثلاث أو أربع إجرائيات في تعليمة واحدة، فتجتاز التعليمة الواحدة الفاصل العمودي على يمين المحرر والمحدد بثمانين حرفا مما يؤثر على مقروئية البرنامج و تتبع خطواته (أو هكذا يقولون).

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

تحويل الأرقام إلى حروف

بدأت بموضوع تحويل الأرقام إلى حروف. و بالذات مع الدالة IntToStr التي تقوم بتحويل رقم ذو عدد صحيح إلى أحرف نصية، مثل التالي:

Caption := IntToStr(i);

هذه الدالة تتكون من ثمانية أحرف، فقمت بتغليفها wrap داخل دالة function يكون إسمها أقصر، فاخترت الاسم "_S" بحيث تكون كالتالي:

function _S(const X: integer): string;
begin
  result := IntToStr(X);
end;

بذلك كلما أردت تحويل عدد صحيح إلى نصّ أستدعي هذه الدالة:

Caption := _S(i);

لكن هذه فقط تقوم بتحويل العدد الصحيح Integer ماذا لو كان العدد من نوع Single ؟

لحسن الحظ كل هذه الأنواع يمكن جمعها تحت النوع Extended وإنشاء دالّة لتحوبل هذا النوع وتسمية هذه الدالّة بنفس اسم الدالّة الأولى أي _S مع تزيينها بأمر overload . أولا قمت بإضافة الأمر overload للدالة السابقة:

function _S(const i: integer): string; overload;

ثم عرّفت دالّة أخرى مع محدد parameter من نوع extended :

function _S(const AValue: Extended): string; overload;

وهذا جسم الدالّة:

function _S(const AValue: Extended): string
begin
  result := FloatToStr(AValue);
end;

لم لا نضع مزيدا من السكّر ونصنع دالّة أخرى نجعلها تقوم بضبط عدد الخانات بعد الفاصلة:

function _S(const AValue: Extended; Digits: Integer): string
begin
  result := FloatToStrF(AValue, ffFixed, 16, Digits);
end;

بهذا يمكن تطبيق هذه الدالّة على الرقم 45687.245654 بحيث تعطينا تمثيل نصي بثلاث خانات بعد الفاصلة

_S(45687.245654, 3)

فنتحصّل على 45687.246.

من بين الأنواع الأخرى التي احتجت لتسهيل عملية تحويلها إلى نص؛ النوع OleVariant، والتي كثر استخدامي لها عند التعامل المباشر مع قيم الحقول في قواعد البيانات تحت مظلة ADO. أيضا كانت عونا كبيرا لي لاختصار الوقت ومجهود الكتابة عند التعامل مع قيم عناصر XML:

function _S(const Value: OleVariant): string;
begin
  if Value <> null then
    result := Value
  else
    result := '';
end;

التحويل إلى نص بنفس مسمّى الدالة يمكن أن يشمل التركيبات records ، مثلا هذه الدالة تقوم بتحويل قيمة من نوع TPoint إلى نص:

function _S(const APoint: TPoint): string;
begin
  Result := Format('(%d,%d)', [APoint.X, APoint.y]);
end;

لأحصل على تمثيل نصي لقيمة من نوع TPoint مثل : (400,600)

و أخرى لمصفوفة من عناصر نوع TPoint:

function _S(const Points: array of TPoint): string;
var
  i: integer;
begin
  Result := '';
  for i := 0 to high(Points) do
  begin
    Result := result + Format('(%d,%d) ',[Points[i].X, Points[i].y]);
  end;
  Result := Trim(result);
end;

أيضا أخرى خاصة بالنوع TRect :

function _S(const ARect: TRect): string;
begin
  Result := Format('(%d,%d)',[ARect.TopLeft.X, ARect.TopLeft.Y]) + ' ' +  
            Format('(%d,%d)',[ARect.BottomRight.X, ARect.BottomRight.Y]);
end;

هذه التحويلات تسهّل العمل كثيرا عندما يتعلق الأمر بتسجيل الكيانات التي تحوي قيم من هذه الأنواع في ملفات نصية مثل ملفات log أو عند عمليات serialization والتحويل من كائنات إلى صيغ أخرى مثل XML .

تحويل نوع التاريخ

للأسف لم أستطع صنع دالّة لتحويل نوع التاريخ إلى نصّ تحت نفس الإسم أي: _S ، وذلك لأن نوع التاريخ TDateTime هو في الأصل من نوع Double ، ولهذا إذا مررّنا للدالة قيمة من نوع TDateTime فربما يتعامل معها كرقم وتعطينا التمثيل النصي لهذا الرقم، كذلك إذا مرّرنا قيمة من نوع Double فربما تأتينا النتيجة على صيغة نصية للتاريخ والوقت. الكود التالي يوضح هذا الإشكال:

var
  Adouble: double;
begin
  Adouble := 123456.123456;
  
  caption := _S(Now)  + ' * ' +
             _S(123456.123456) + ' * ' + 
             _S(Adouble);

الناتج سيكون:

'2016-03-19 01:02:01 * 123456.123456 * 2238-01-03 02:57:46'

لاحظ أن الناتج يختلف بين القيمة التي مرّرت مباشرة للدالة و تلك التي تمّ تمريرها كمتغيّر من نوع double يحمل نفس القيمة.

لذلك استحدث دالّة بإسم آخر : _SD لتحويل التاريخ إلى نص:

function _SD(const ADateTime: TDateTime): string;
begin
  result := DateTimeToStr(ADateTime);
end;

التحويل لأرقام

بنفس السياق قمت باستحداث دوال لتحويل نوع نصي string إلى نوع integer بتسمية _i مثل:

function _I(const S: string): integer;
begin
  result := StrToIntDef(S, 0);
end;

و أخرى لتحويل الأرقام العشرية إلى عدد صحيح:

function _I(const AValue: real): integer;
begin
  result := Round(AValue);
end;

كذلك تحويل الصيغ النصية للتاريخ إلى نوع تاريخ، بدالة ذات اسم مختصر:

function _D(S: string): TDateTime;
begin
  result := StrToDateTime(s);
end;

من الأسماء إلى الأفعال

ما قمت به لحدّ الآن هو إنشاء أسماء موجزة لدوال ذات أسماء طويلة.

ماذا عن المهام ذات التعليمات الطويلة، والتي يتكرّر استخدامي لها؟ ماذا لو قمت بتغليف تعليمات هذه المهام داخل دوال مختصة، بحيت كلما احتجت لتنفيذ مهمة منها أكتفي فقط باستخدام الدالة الخاصة بها دون الخوض في تفاصيل تعليماتها.

لنجرّب:

مربعات حوار الرسائل Message dialog box

إذا كنت مثلي مما يفضلون استخدام مربعات حوار الرسائل الأصلية الخاصة بويندوز؛ فحتما تعلم مدى تعدد وتنوع المعطيات والخيارات اللازم إعدادها لإنشاء مربع رسالة بسيطة مثل هذه:

لإنشاء هذه الرسالة يتطلّب الأمر الكود التالي:

sMsg := 'مرحبا.. هذه رسالة ترحيب بسيطة!';
sTitle := 'رسالة ترحيب';  
result := MessageBoxW(Application.Handle, PWideChar(sMsg),
                        PWideChar(sTitle),
                        MB_ICONINFORMATION +
                        MB_Right +
                        MB_RTLREADING);
    

ويزداد الأمر إرباكا إذا أرت أنواعا أخرى من الرسائل للتنبيه أو الخطأ، أو الطلب من المستخدم الاختيار بين عدة خيارات باالموافقة أو الإلغاء أو تجاوز الأمر، فكل هذه الخيارات عليك أن تصيغها ضمن أمر واحد وهو: MessageBox . لذا وجدت أن الأمر سوف يكون أكثر سهولة لو قسمت أنواع الرسائل بحيث يكون لكل نوع دالّة خاصة به، مثلا واحدة لرسالة استعلام بنعم أو لا، وأخرى تنبيه بموافق و إلغاء الأمر، وهكذا.

بدأت بدالّة عامة أسميتها Msg مشابهة لدالة MessageBox وبنفس محدداتها عدا أن محدّدا نصّ الرسالة والعنوان جعلتهم من نوع string بدل PChar لتسهيل مخاطبتها. هذه الدالة العامة سوف أستخدمها لكي يتم مناداتها من داخل الدوال الأخرى التي اعتزم إنشاؤها.

function Msg(Handle: integer; sMsg, sTitle: Widestring; iType: integer): integer;
begin
  result := MessageBoxW(Handle, PWideChar(sMsg), PWideChar(sTitle), iType);
end; 

ثم بدأت بإنشاء دالة لعرض نص رسالة مع أيقونة information وزرّ موافق كالتالي:

function Msg(sMsg: WideString): integer;
begin
  result := Msg(Application.Handle, sMsg, Application.Title, MB_ICONINFORMATION);
end;

بهذه الدالة يمكنني الآن أن أنشئ مربع رسالة بسيط بأقل ما يمكن من كود مثل التالي:

Msg('Hi Delphi programmers!');

لأتحصّل عل مربع الرسالة التالية:

وهذه :

function MsgYesNoCancel(sMsg, sTitle: WideString): integer;
begin
  result := Msg(Application.Handle, sMsg, sTitle, MB_YESNOCANCEL + MB_ICONQUESTION);
end;
MsgYesNoCancel('Hi Delphi programmers!' + #13 + 'Shall I continue?', 'Confirm');

كل دالة لها أخت لها تشبهها لكنها معدةّ للعرض من اليمين لليسار لتكون مناسبة للرسائل باللغة العربية.

function MsgYesNoCancelR(sMsg, sTitle: WideString): integer;
begin
  result :=  Msg(Application.Handle, sMsg, sTitle, MB_YESNOCANCEL
                                                 + MB_ICONQUESTION
                                                 + MB_RIGHT
                                                 + MB_RTLREADING);
end;

وهذا مثال على تنفيذها:

if MsgYesNoCancelR('مرحبا'+ #13 + 'هل ترغب في الاستمرار؟' , 'تأكيد') =
          ID_YES then
  Msg('حسنا، سوف نواصل!');

برغم طول أسماء هذه الدوال حتى يسهل الاستدلال على مهامها؛ إلا أن تذكّر أسماء هذه الدوال لن يكون مشكلة؛ حيث أن كلّ دالة تبدأ ب Msg وبمجرد كتابتها فإن قائمة الاستشعار الذكي للكود intellisense سوف تظهر كافة الدوال التي تبدأ بنفس الأحرف للإختيار من بينها

المزيد من مربعات الحوار

جانب آخر من مربعات الحوار التي أحتاجها دائما هي مربع حوار فتح ملف TOpenDialog و حفظ ملف TSaveDialog ، هذا النوع من مربعات الحوار متوفرة في دلفي كمكوّن مرئي مثل TOpenDialog:

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

sFileName := DialogOpenFile('');

كي أحصل على هذه النتيجة:

تستقبل الدالّة DialogOpenFile معطى argument نصي واحد يتم فيه تحديد نوع الملفات التي يتم فرزها عن غيرها، مثل:

sFileName := DialogOpenFile('Doc Files|*.doc; *.docx|All Ailes|*.*');

في حالة عدم تحديد نوع الملفات كما في مثالنا الأول؛ تقوم الدالة بافتراض أن الملفات المفروزة ستكون بامتداد .txt فيما يلي كود الدالّة:

function DialogOpenFile(Filter: string): string;
var
  dlg: TOpenDialog;
begin
  dlg := TOpenDialog.Create(nil);
  try
    if Trim(Filter) = '' then
      Filter := 'Text Files|*.txt|All Ailes|*.*';

    dlg.Filter := Filter;
    if dlg.Execute then
      result := dlg.FileName
    else
      result := '';
  finally
    dlg.Free;
  end;
end;

وبنفس التعليمات تقريبا؛ يمكن إنشاء دالّة أخرى خاصة بمربع حوار حفظ ملف مع استبدال مكون TSaveDialog ب TOpenDialog .

الملفات النصية

المجال التالي التي سأتعامل معه في سعيي لتسهيل المهام البرمجية المتكرّرة هي الملفات النصية.

توجد أكثر من طريقة للحصول على محتوى ملف نصي، لكن ربما أسهلها هو استخدام صنفية class TStringList . لكن التعامل معها يتطلب بعض الخطوات من إنشاء لكائن object من نوع هذه الصنفية، ثم استخدامه لتحميل الملف النصي، وأخيرا تحرير الذاكرة من الكائن وإفنائه. الكود التالي يوضح ذلك في أبسط صورة، وبأقل ما يمكن من خطوات:

***
 var
  st: TStringList;
  FileName: string;
  AText: string;
 begin
  FileName :=  'Test.txt';
  st := TStringList.Create;
   try
    st.LoadFromFile(FileName, TEncoding.UTF8);
    AText := st.Text;
   finally
    st.Free;
   end; 
  ***
  ***

كما نرى خطوات كثيرة لمجرد الحصول على المحتوى النصي للملف.

ما أرغب فيه هو تعليمة بسيطة تغنيني عما سبق مثل التالي:

var
  AText: string;
begin
  AText := FileText('Test.txt');
  ***

حيث يكفي استدعاء دالة واحدة مع إعطائها إسم الملف ، فتقوم الدالة بكل مايلزم وترد لي بالمقابل النص الذي بالملف في متغير.

قبل الشروع في صنع هذه الدالة؛ أحتاج لصنع دالة مساعدة تكون الأساس لصنع دالة FileText ودوال أخرى تقدّم لي محتوى النص بصيغ أخرى. الدالّة المساعدة إسمها FileStrings :

function FileStrings(const FileName: string): TStrings;
var
  sl: TStringList;
begin
  result := nil;
  if not FileExists(FileName) then exit;

  sl := TStringList.Create;
  tryexcept
    sl.Free;
    sl := nil;
  end;
end;

الدالّة تستقبل إسم ملف المطلوب فتحه، وناتجها result هو كائن من نوع TStrings . بعد أن تقوم الدالة بالتأكد من وجود الملف، تقوم بإنشاء كائن من نوع مشتق وهو TStringList ليقوم بفتح وتحميل الملف. إذا نجحت العملية يتم إسناد هذا الكائن إلى ناتج الدالّة، في حالة حدوث أي خطأ يتم إفناء الكائن وإعادة قيمته إلى لا شيئ nil.

(لاحظ إننا لم نقم بإفناء الكائن في حالة نجاح عملية فتح الملف، وتركنا مسؤولية هذا الأمر لمن يقوم بإستدعاء هذه الدالة). بعد هذا نشرع في بناء الدالة FileText كالتالي:

function FileText(const FileName: string): string;
var
  Strings: TStrings;
begin
  result := '';
  try
    Strings := FileStrings(FileName);
    if Strings <> nil then
      result := Strings.Text;
  finally
    if Strings <> nil then
      Strings.Free;
  end;
end;

كما نرى؛ تقوم الدالة بتطبيق الدالة المساعدة FileStrings على إسم ملف، فتتحصّل على قائمة نصية من نوع TStrings بمحتويات الملف (قائمة بأسطر المحتوى النصّي للملف) ، ثم باستخدام خاصية Text يتم اسناد كامل المحتوى في متغير واحد result من نوع string الذي هو ناتج الدالة.

(لاحظ كيف أن هذه الدالة مسؤولة عن إفناء الكائن Strings المتحصّل من دالة FileStrings)

بهذا تكون الدالة قد سهّلت علينا الحصول على محتويات ملف نصي ووضعه في متغير من نوع string

AText := FileText('Test.txt');
Memo1.Text := AText;

ماذا لو أردنا الحصول على محتوى نفس الملف في مصفوفة بحيث تنتظم أسطر نصّ الملف في عناصرها حيث كل عنصر في المصفوقة يمثل سطرا في النص؟

الدالة التالية ستقوم بالمطلوب وتعطي مصفوفة نصية بأسطر الملف:

function FileArray(const FileName: string): TStringArray; 
var
  Strings: TStrings;
  i: integer;
begin
  result := 0;
  try
    Strings := FileStrings(FileName);
    if Strings <> nil then
    begin
      SetLength(result, Strings.Count);
      for i := 0 to Strings.Count - 1 do
        result[i] := Strings[i];

    end;
  finally
    if Strings <> nil then
      Strings.Free;
  end;
end;

الدالة ترجع مصفوفة حيوية dynamic array من نوع TStringArray والذي نحتاج إلى تعريفه قبل استخدامه في بناء الدالة، لذلك يجب وضع التعريف في مكان يكون في منظور الدالة ومنظور من يستدعيها:

type
  TStringArray = array of string;

وكما في دالة FileText السابقة تقوم الدالة بالاستعانة ب FileStrings لتردّ لها قائمة نصية من نوع TStrings ، وبناء على عدد عناصر القائمة تحدد طول المصفوفة بنفس العدد، ثم تنسخ محتويات كل عنصر في القائمة إلى ما يقابلها من عناصر المصفوفة.

يمكن الآن استخدام هذه الدالة كما في المثال التالي:

procedure TForm1.Button1Click(Sender: TObject);
var
  arrItems: TStringArray;
  i: integer;
begin
  arrItems := FileArray(‘sample.txt’);
  Memo1.Clear;
  for i := 0 to High(arrItems) do
    Memo1.Lines.Add(arrItems[i]);
end;

وأخيرا

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

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

في ختام مقالتي أودّ الإشارة إلى تعليمة أخرى نسيت ذكرها، تعليمة مزعجة يكثر استخدامي لها؛ هي تعليمة UpperCase . كالعادة، اختصرتها في دالّة _U :

function _U(const s: string): string;
begin
  result := AnsiUpperCase(s);
end;