مقال تقني

شبكة جداول بيانات مخصصة في Delphi عبر HotXLS

يشحن HotXLS TXLSWorkbookViewer، عنصر تحكم VCL أصلي يرسم مصنّفات عمل XLS وXLSX وXLSM وODS كشبكة جدول بيانات تفاعلية داخل نموذج Delphi أو C++Builder، دون تثبيت Excel أو تشغيله عبر أتمتة OLE. بناء نوع عنصر التحكم هذا بشكل جيد يعني حل ثلاث مشكلات محددة: تحويل نقرة ماوس تقع داخل خلية مدمجة إلى الخلية المنطقية الصحيحة، والحفاظ على موضع التمرير، وأشرطة الترويسة، وتحديد الخلايا متسقة أثناء تحريك المستخدم بانوراميًا لورقة أكبر بكثير من النافذة المرئية، وتقرير ما يجب أن تفعله فعليًا نقرة على علامة تعليق أو خلية رابط تشعبي

يلجأ معظم متاجر Delphi إلى عارض جدول بيانات لأسباب لا علاقة لها بالتحرير: محطة تدقيق تعاين مصنّفات العمل المرفوعة قبل دخولها خط أنابيب، أو كشك أو عارض تقارير حيث Microsoft Office ليس جزءًا من صورة النشر، أو أداة ضمان جودة تحتاج عرض محتويات مصنّف عمل دون عدم القابلية للتنبؤ في أتمتة عملية Excel حقيقية عبر COM. تمنحك شبكة نصية عادية نصًا في الخلايا بسرعة، لكن ملف جدول بيانات ليس شبكة عادية: تندمج الخلايا في كتل لا توجد إلا مرة واحدة في النموذج الكامن، وتحمل الأوراق أشرطة ترويسة ثابتة ومواضع تمرير أفقية ورأسية مستقلة، وتحمل خلايا فردية تعليقات وروابط تشعبية تحتاج نموذج تفاعل خاصًا بها. TXLSWorkbookViewer هو إجابة HotXLS عن تلك الفجوة، وتصميمه الداخلي مخطط معقول لأي شخص يبني عنصر تحكم مماثلًا من الصفر

كيف يتجنب عارض مصنّف العمل الاعتماد على Excel؟

يتجنب TXLSWorkbookViewer Excel كليًا بالقراءة عبر نموذج الكائنات المُحلَّل الخاص بـHotXLS نفسه بدلًا من فتح مستند عبر Excel وتحريكه كدمية. تربط خاصية Workbook مصنّف عمل TXLSWorkbook موجودًا لملفات XLS الكلاسيكية، وتربط XlsxWorkbook مصنّف عمل TXLSXWorkbook لملفات XLSX وXLSM وأشكال القوالب؛ يمكن أن يكون أي منهما مفتوحًا بالفعل في مكان آخر من التطبيق، ولا يفعل العارض سوى القراءة منه. عندما يجب أن يمتلك عنصر التحكم الملف نفسه، تفحص LoadFromFile الامتداد، وتوجّه XLSX وXLSM وXLTX وXLTM وODS عبر المحرك الحديث وكل شيء آخر عبر الكلاسيكي، وتحرر أيًّا كان مصنّف العمل الذي أنشأته بمجرد مسح عنصر التحكم أو تدميره

var
  Viewer: TXLSWorkbookViewer;
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  if Book.Open('quarterly-report.xlsx') <> 1 then
    raise Exception.Create('Could not open workbook');

  Viewer := TXLSWorkbookViewer.Create(Self);
  Viewer.Parent := Self;
  Viewer.Align := alClient;
  Viewer.XlsxWorkbook := Book;        // the viewer does not take ownership
  Viewer.GoToCell(1, 1);

  Caption := Viewer.WorksheetName + ': ' + Viewer.SelectedCellText;
end;

تحديد الخلية الصحيحة داخل نطاق مدمج

حل نقرة إلى الخلية الصحيحة في TXLSWorkbookViewer بحث من مرحلتين، والانقسام يهم لأن هندسة البكسل وسمانتيك جدول البيانات مشكلتان مختلفتان حقًا. المرحلة الأولى هندسة بحتة: تجتاز طريقة خاصة CellAtPoint عروض الأعمدة وارتفاعات الصفوف من موضع التمرير الحالي حتى تجد الشريط الذي يحتوي إحداثيي X وY المنقور عليهما، دون أي وعي بالخلايا المدمجة إطلاقًا. المرحلة الثانية سمانتيكية: كل مسار يغيّر التحديد، نقرة ماوس، أو مفتاح سهم، أو Tab، أو استدعاء مباشر لـGoToCell، يصبّ عبر روتين داخلي واحد ChangeSelection، الذي يُطبِّع الصف والعمود الخام مقابل أي دمج ويُثبّتهما على خلية مرساة الدمج قبل أن يتغير التحديد فعليًا

المرساة هي الخلية العلوية اليسرى من النطاق المدمج، وهي الخلية الوحيدة في تلك الكتلة التي تحمل حقًا قيمة، أو صيغة، أو تعليقًا، أو رابطًا تشعبيًا في نموذج مصنّف العمل الكامن؛ كل خلية أخرى يغطيها الدمج بصريًا فارغة في البيانات نفسها. بالنسبة لمصنّفات عمل XLS الكلاسيكية، تأتي المرساة من Cell.MergeArea، وهو IXLSRange تشير Row وColumn الخاصتان به إلى الخلية المالكة؛ وبالنسبة لمصنّفات عمل XLSX وODS، تُعيد MergedCells.FindAt كائن TXLSXMergedRange يعرض المرساة نفسها كـRow1 وCol1. يحل الرسم مشكلة مكافئة بشكل مستقل، بتوسيع مستطيل خلية مدمجة إلى امتداد صفها وعمودها الكامل وتخطي الخلايا داخل ذلك الامتداد، بحيث يلتف مخطط التحديد حول الكتلة المدمجة بأكملها لا زاوية مرساتها فقط، وكتابة تخطيطات مدمجة بدلًا من مجرد قراءتها مرة أخرى مشكلة ذات صلة لكنها مميزة مشروحة في المقالة المرافقة عن تخطيط الخلايا المدمجة لقوالب التقارير

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Book.Sheets.Add('Summary');
  Sheet.MergeCells(2, 2, 3, 4);       // B2:D3
  Sheet.Cells[2, 2].Value := 'Region totals';

  Viewer.XlsxWorkbook := Book;
  Viewer.GoToCell(3, 4);              // targets the bottom-right corner of the merge
  // SelectedRow is now 2 and SelectedCol is now 2: normalized to the anchor cell
end;

ما الذي يبقي التمرير والترويسات والتحديد متزامنة؟

يحافظ TXLSWorkbookViewer على تناسق ثلاث قطع منفصلة من الحالة: موضع التمرير المنطقي المحفوظ في TopRow وLeftCol، وأشرطة التمرير الأصلية في ويندوز التي يطلبها عنصر التحكم عبر WS_HSCROLL وWS_VSCROLL في CreateParams، والتحديد الحالي في SelectedRow وSelectedCol. سحب شريط تمرير أو تدوير عجلة الماوس يُطلق WM_HSCROLL، أو WM_VSCROLL، أو WM_MOUSEWHEEL، وهو ما يحدّث TopRow أو LeftCol ويعيد الرسم؛ ولا يتحرك التحديد، وهو ما يطابق طريقة Excel نفسه في الفصل بين التحريك البانورامي والتحديد. بعد أي من تلك التحديثات، تدفع UpdateScrollBars الموضع الجديد مرة أخرى إلى شريط التمرير الأصلي عبر SetScrollInfo، بحيث لا ينحرف الإبهام أبدًا عن اتفاقه مع ما تعرضه الشبكة فعليًا

يُشغّل التنقل بلوحة المفاتيح المزامنة نفسها بالاتجاه المعاكس: تحريك التحديد متجاوزًا حافة الشبكة المرئية يستدعي EnsureSelectionVisible، التي تدفع TopRow أو LeftCol بتراكم عروض الأعمدة وارتفاعات الصفوف الفعلية بدلًا من مجرد الزيادة بواحد، بما أن الصفوف والأعمدة يمكن أن تحمل أحجامًا مخصصة، ثم تستدعي UpdateScrollBars بحيث يعكس الإبهام أيًّا كان المكان الذي أخذت إليه لوحة المفاتيح العرض للتو. أشرطة ترويسة رقم الصف وحرف العمود، المُحجَّمة عبر RowHeaderWidth وColumnHeaderHeight، هي الجزء من عنصر التحكم هذا الذي يبقى ثابتًا على الشاشة بينما تُمرِّر TopRow وLeftCol البيانات أسفله، وهذا مدى التجميد الذي يفعله عنصر التحكم هذا بنفسه: إنه ليس ميزة Freeze Panes الخاصة بـExcel، ولا توجد طريقة مدمجة لتثبيت صف أو عمود بيانات عشوائي بينما تمر بقية الورقة به. حد واحد يستحق الاختبار قبل نشر عارض على ملفات لا تتحكم بها بالكامل هو أن TopRow وLeftCol غير مقيّدين مقابل النطاق المستخدم الفعلي لورقة العمل، بحيث يمكن لإبهام سُحب إلى حده البنيوي أن يقع على الصف 1,048,576 أو العمود 16,384 ويعرض شبكة فارغة بدلًا من آخر صف أو عمود يحمل بيانات فعليًا؛ مصنّفات العمل الكبيرة بما يكفي لجعل ذلك ملحوظًا عادة ما تكون أيضًا كبيرة بما يكفي لتحتاج الانتباه على جانب التحميل المشروح في مقالة أداء مصنّفات العمل الكبيرة

ربط التعليقات والروابط التشعبية بأحداث الماوس والتحديد

يعامل TXLSWorkbookViewer التعليقات والروابط التشعبية كخصائص لأيًّا كانت الخلية المحددة حاليًا بدلًا من أهداف تحوّم، بحيث تُحدَّث SelectedCellCommentText وSelectedCellCommentAuthor وSelectedCellHyperlink في كل مرة يُطلَق فيها OnSelectionChange، سواء تحرك التحديد بنقرة ماوس، أو مفتاح سهم، أو استدعاء لـGoToCell. تُرسم خلية بها تعليق مثلثًا أحمر صغيرًا في زاويتها العلوية اليمنى كإشارة بصرية، مشابهة لعلامة التعليق في Excel نفسه، لكن تلك العلامة بصرية بحتة؛ لا توجد تلميحة أدوات مُشغَّلة بالتحوّم مدمجة في عنصر التحكم، بحيث يتعين على تطبيق يريد نافذة منبثقة عند تحوّم الماوس بدلًا من عند التحديد بناء تلك الطبقة بنفسه. يعمل تفعيل الرابط التشعبي بالطريقة نفسها التي يعطي فيها التحديد الأولوية: النقر المزدوج على خلية يستدعي ActivateSelectedCell، التي تقرأ SelectedCellHyperlink، وإذا لم تكن فارغة، تُطلِق OnHyperlinkClick بعنوان الهدف ومعامل var Handled: Boolean ليضبطه المعالج

ما لا تفعله OnHyperlinkClick يهم بقدر ما تفعله: لا يستدعي TXLSWorkbookViewer أبدًا ShellExecute ولا يفتح متصفحًا بنفسه، بصرف النظر عمّا إذا ضبط المعالج Handled على صحيح أو تركها خاطئة. التنقل، وأي قرار حول ما يُحسب هدفًا آمنًا، هو مسؤولية التطبيق المضيف بالكامل، وهو الافتراضي الصحيح لمكوّن لا يعرف أبدًا ما إذا كان مُضمَّنًا في أداة داخلية موثوقة أو عارض لملفات رفعها عميل للتو

procedure TMainForm.ViewerSelectionChange(Sender: TObject; Row, Col: Integer);
begin
  if Viewer.SelectedCellCommentText <> '' then
    StatusBar.SimpleText := Viewer.SelectedCellCommentAuthor + ': ' +
      Viewer.SelectedCellCommentText
  else
    StatusBar.SimpleText := Viewer.SelectedCellHyperlink;
end;

procedure TMainForm.ViewerHyperlinkClick(Sender: TObject;
  const Target: WideString; var Handled: Boolean);
begin
  ShellExecute(0, 'open', PWideChar(Target), nil, nil, SW_SHOWNORMAL);
  Handled := True;
end;

نطاق التحديد وحدود التنقل بلوحة المفاتيح

التحديد في TXLSWorkbookViewer دائمًا خلية منطقية واحدة، تُتابَع كـSelectedRow وSelectedCol؛ لا يوجد تحديد نطاق مستطيل متعدد الخلايا في عنصر التحكم الأساسي، بحيث يجب بناء أي ميزة تحتاج العمل على كتلة من الخلايا فوقه بدلًا من قراءتها من كائن تحديد. تغطية لوحة المفاتيح أساسية عمدًا: تحرك مفاتيح الأسهم خلية واحدة في كل مرة، وتعيد Home إلى بداية الصف، أو مع Ctrl، إلى الخلية A1، وتقفز Page Up وPage Down عشرة صفوف، وتتنقل Tab وShift+Tab عبر الأعمدة؛ لا توجد قفزة Ctrl+سهم إلى حافة منطقة بيانات ولا تحديد نطاق ممتد بـShift، بحيث سيلاحظ المستخدمون القادمون مباشرة من Excel الفجوة على ورقة كثيفة

تُفرض حدود الأعمدة عند نقطة الاختناق نفسها ChangeSelection التي تعالج تطبيع الدمج، وتختلف بحسب المحرك عمدًا: عارض مربوط بـTXLSWorkbook كلاسيكي يُقيَّد عند العمود 256، السقف البنيوي لصيغة BIFF8، بينما عارض مربوط بـTXLSXWorkbook يحترم حد الـ16,384 عمودًا الحديث الذي ورثته XLSX من Excel 2007 فصاعدًا. تُحدَّد الصفوف عند 1,048,576 في كلتا الحالتين، بحيث يكون الفرق العملي بين فتح ملف XLS قديم وملف XLSX في العارض نفسه بالكامل حول مدى استعداد الشبكة للسماح لك بالذهاب يمينًا

لا شيء من هذا غريب بمجرد تفكيكه إلى بحث بكسل، وتطبيع مرساة، وحفنة من معالجات الرسائل، لكن جعل الثلاثة تتفق تحت ملفات حقيقية، بدمج حقيقي، وتعليقات، وروابط تشعبية، هو معظم العمل في مكوّن كهذا. يُشحن TXLSWorkbookViewer كجزء من مكوّن HotXLS لـExcel القياسي لـDelphi وC++Builder، إلى جانب نماذج الكائنات الكلاسيكية وXLSX التي يرسم منها