انسخ نطاقًا من شبكة Delphi وألصقه في Word، وعادة ما يختفي التنسيق: نص عادي، بلا ترويسات عريضة، بلا حدود، بلا تعبئات. يغلق HotXLS تلك الفجوة عبر TXLSRange.CopyToClipboard، التي تضع حمولة حافظة CF_HTML — صيغة ويندوز لـHTML المنسّق بعلامات جزء دقيقة بالبايت — على الحافظة إلى جانب نص يونيكود عادي
يبدو هذا بسيطًا حتى تنظر إلى ما تتطلبه حمولة CF_HTML فعليًا. تحتاج الصيغة ترويسة نصية قصيرة تسمّي بالضبط أين يبدأ الجزء وينتهي داخل المخزن المؤقت الأكبر للحافظة، وتلك المواضع إزاحات بايت، تُحسب عبر أيًّا كان الترميز متعدد البايتات الذي ينتهي إليه HTML. أخطئ في الحساب ببايت واحد فقط وسيلتقط التطبيق الهدف إما الشريحة الخاطئة من الترميز أو يستسلم ويعود إلى نص عادي، ولا يبدو أي من الفشلين كخلل في شيفرتك — يبدو كأن Word يتصرف كعادة Word
لماذا يفقد النسخ واللصق من شبكة Delphi تنسيقه عادة
استدعاء الحافظة الافتراضي في ويندوز الذي تلجأ إليه معظم شيفرات Delphi، SetClipboardData مع CF_TEXT أو CF_UNICODETEXT، لا يحمل أبدًا سوى محارف عادية، بحيث لا يكون لأي تنسيق مطبَّق في شبكة المصدر مكان يذهب إليه. يبحث Word وOutlook وكل متصفح قائم على Chromium عن صيغة أغنى عند اللصق: تمثيل HTML للتحديد، كاملًا بأنماط مضمّنة، وبنية جدول، وروابط. يعتمد Excel نفسه على هذه الحيلة بالضبط — انسخ نطاقًا في Excel وستستقبل الحافظة بصمت عدة صيغ دفعة واحدة، من ضمنها HTML، بحيث يختار أي تطبيق تلصق فيه الأغنى الذي يفهمه. مكوّن لا يكتب أبدًا سوى CF_UNICODETEXT لا يسلّم أيًّا من تلك المستهلكات الأغنى شيئًا للعمل عليه، والثراء البصري الذي نسخه المستخدم للتو ببساطة غير موجود للصقه
ما هي صيغة الحافظة CF_HTML بالضبط؟
ليست CF_HTML صيغة حافظة نظامية ثابتة مثل CF_TEXT؛ إنها مسجَّلة ديناميكيًا، تُطلَب بالاسم عبر RegisterClipboardFormat('HTML Format')، وحمولتها ترويسة ASCII قصيرة تليها وثيقة أو جزء HTML. تحمل الترويسة خمسة حقول — Version، وStartHTML، وEndHTML، وStartFragment، وEndFragment — حيث Version دائمًا 0.9 والأربعة الأخرى أرقام عشرية مكتوبة كأرقام ASCII. تحدّ StartHTML وEndHTML الوثيقة بأكملها كما ينبغي للتطبيق المستقبِل تحليلها للسياق، بما في ذلك الخطوط والأنماط، بينما تحدّ StartFragment وEndFragment الشريحة الأضيق التي تقع فعليًا عند المؤشر، مُعلَّمة اصطلاحيًا في الترميز نفسه بتعليقات <!--StartFragment--> و<!--EndFragment--> بحيث تنجو الحدود من إعادة التسلسل الساذجة
إزاحات بايت، لا عدد محارف: الفخ الكلاسيكي في CF_HTML
حقول ترويسة CF_HTML الرقمية الأربعة هي إزاحات بايت في تسلسل البايتات الدقيق الجالس على الحافظة، تُحسب من أول محرف في الترويسة نفسها — لا عدد محارف، ولا نقاط شيفرة يونيكودية، ولا إزاحات بالنسبة للجزء أو وسم <body>. ذلك التمييز هو حيث تُخطئ تنفيذات CF_HTML المكتوبة يدويًا بصمت: تُبلّغ خاصية Length لسلسلة UnicodeString في Delphi عن وحدات شيفرة UTF-16، وهو ما يصادف أن يساوي عدد البايتات لنص ASCII عادي، بحيث يمر الخلل نظيفًا عبر أي اختبار مكتوب ببيانات عينة إنجليزية ولا يظهر إلا بمجرد أن تحمل خلية منسوخة شرطة em، أو رمز عملة، أو حرفًا منقوطًا — علامة اليورو وحدة شيفرة UTF-16 واحدة لكنها ثلاثة بايتات في UTF-8، وكل إزاحة محسوبة بعد تلك النقطة تنحرف بمقدار أيًّا كان عدد البايتات الإضافية التي أضافها الترميز. الفشل الذي يتبع ذلك ليس تعطلًا؛ إنه استيلاء التطبيق المستقبِل على نطاق البايت الدقيق الذي أشارت إليه الترويسة، وإيجاده شريحة من الترميز تبدأ أو تنتهي في منتصف وسم، وإما رسم عبث أو الاستسلام والعودة إلى أيًّا كان النص العادي الجالس بجواره على الحافظة، بصمت، دون أي شيء في شيفرتك لتفسير السبب — إليك شكل الشيفرة التي تنتج بالضبط ذلك الفشل
// Fragile: Length() on a UnicodeString counts UTF-16 code units, not bytes
var
Header: string;
Fragment: string;
StartFragmentOfs: Integer;
begin
Header := 'Version:0.9'#13#10 + 'StartHTML:0000000000'#13#10 + '...';
StartFragmentOfs := Length(Header) + Pos('<!--StartFragment-->', Fragment);
// A currency symbol, an em dash, or any accented character placed
// before this point costs one character here but two or three bytes
// once the document is UTF-8 encoded, so StartFragmentOfs now points
// short of where the fragment actually begins on the real clipboard
end;
كيف يحافظ HotXLS على دقة الترويسة بالبايت
يتجنب HotXLS هذه الفئة من الأخطاء بنيويًا: تبني TXLSRange.CopyToClipboard ووحدة lxClipboard تحتها وثيقة CF_HTML وترويستها بأكملها كـAnsiString، نوع سلسلة البايتات في Delphi، بحيث تُعيد Length وPos بالفعل مواضع بايت في كل مكان في الحساب — لا توجد خطوة منفصلة، وبالتالي لا خطوة يمكن نسيانها، حيث يحتاج عدد محارف يونيكود للتحويل إلى عدد بايتات قبل دخوله الترويسة
توجد حيلة ثانية أصغر تستحق المعرفة إن بنيت يومًا ترويسة CF_HTML يدويًا. تُكتب الترويسة مرتين: مرة بعشرة أرقام صفرية تقف مكان كل من الإزاحات الأربعة، بحيث يمكن قياس طولها بالبايت الخاص، ومرة أخرى بالإزاحات الحقيقية مُرقّعة فيها. ولأن كل إزاحة حقيقية تُنسَّق إلى عرض العشرة أرقام الثابت نفسه، تخرج الترويسة الثانية بالطول نفسه بايتًا لبايت مثل نسخة العنصر النائب، وهذا بالضبط سبب بقاء القياس الأول صالحًا بعد إعادة الكتابة. تخطَّ العرض الثابت، ونسّق رقمًا بـIntToStr عادية بدلًا من ذلك، ويمكن للترويسة أن تنكمش أو تنمو برقم واحد بين التمريرتين، مبطلة بصمت كل إزاحة تليها
const
Placeholder = '0000000000'; // 10 ASCII digits: fixed width in, fixed width out
var
Header: AnsiString; // AnsiString.Length is a byte count, not a char count
StartHtmlOfs: Integer;
begin
Header := 'Version:0.9'#13#10 +
'StartHTML:' + Placeholder + #13#10 +
'EndHTML:' + Placeholder + #13#10 +
'StartFragment:' + Placeholder + #13#10 +
'EndFragment:' + Placeholder + #13#10;
StartHtmlOfs := Length(Header); // safe to measure once, up front
// ...compute the real offsets against the AnsiString document...
// then rebuild Header with the real numbers formatted to the same
// 10-digit width, so its byte length -- and therefore StartHtmlOfs --
// never moves between the placeholder pass and the final one
end;
لماذا لا تزال حمولة النص العادي يجب أن تُرافق
لا تضع TXLSRange.CopyToClipboard أبدًا CF_HTML على الحافظة وحدها؛ فهي تكتب دائمًا CF_UNICODETEXT في الاستدعاء نفسه، لأن CF_HTML صيغة مسجَّلة بدلًا من واحدة من ثوابت CF_* الثابتة التي يعرف كل تطبيق ويندوز بالفعل البحث عنها — محرر نص عادي، أو شبكة قديمة، أو أي شيء لم يتحقق قط من 'HTML Format' لن يراها إطلاقًا، والنطاق الذي نسخته إما يصل كنص مفصول بعلامات جدولة أو لا يصل. ذلك النص المفصول بعلامات الجدولة ليس تقريبًا خشنًا أيضًا: تُنسَخ خلايا الصيغة كسلسلة صيغتها مع استعادة علامة = البادئة إذا أسقطها النص المخزَّن، مطابقة لكيفية تصرف نص حافظة Excel نفسه، وتُنسَخ الخلايا العادية كـFormattedText الخاص بها — السلسلة كما تُعرض، بحيث تُنسَخ خلية عملة كـ$1,234.56، لا القيمة الكامنة 1234.56 — وأي حقل يحتوي علامة جدولة، أو علامة اقتباس، أو فاصل سطر يُوضع بين علامتي اقتباس مع مضاعفة علامات الاقتباس المضمَّنة، الاصطلاح نفسه الذي تستخدمه CSV
ليست SaveAsHTML مسارًا رسم منفصلًا مُلحقًا لحالة الحافظة فقط. تستدعي CopyToClipboard كاتب HTML نفسه بالضبط الموصوف في تصدير HotXLS لـCSV وTSV وHTML، ثم تغلّف أيًّا كان ما ينتجه ذلك الكاتب في مغلّف CF_HTML بدلًا من حفظه كملف مستقل، بحيث ينتقل مباشرة كل ما هو صحيح عن ذلك HTML إلى ما يقع على الحافظة. تجميع نطاق ورقة عمل بكلتا الصيغتين في استدعاء واحد يبدو هكذا
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('quarterly-report.xlsx');
// Classic TXLSWorkbook ranges expose the identical method as
// Workbook.Sheets[1].Range['A1', 'F40'].CopyToClipboard
if Book.Sheets[1].Range['A1:F40'].CopyToClipboard then
ShowMessage('Range copied - press Ctrl+V in Word or a browser')
else
ShowMessage('Clipboard was busy; see the retry pattern below');
finally
Book.Free;
end;
end;
هل يحافظ النطاق المُلصَق على خطوطه وألوانه وخلاياه المدمجة؟
نعم، لأن نصف HTML من الحمولة رسم كامل للنطاق، لا تفريغ بيانات مجرد: الخطوط، وألوان التعبئة، والحدود، وصيغ الأرقام، والخلايا المدمجة كلها تنتقل كأنماط مضمّنة وبنية جدول، آلية التنسيق نفسها المشروحة في دليل HotXLS للتنسيق الشرطي والنص المنسّق، بما أن كلًا من مقاطع النص المنسّق للخلية ونتيجة التنسيق الشرطي يغذّيان الرسم نفسه الذي تقرأ منه CopyToClipboard. ما لا ينجو من الرحلة هو سلوك الصيغة الحية: يحمل الشكل النصي العادي لخلية الصيغة سلسلة الصيغة، بحيث يمكن لهدف لصق واعٍ بجداول البيانات نظريًا إعادة حسابها، لكن شكل HTML لا يحمل أبدًا سوى النتيجة المحسوبة الأخيرة، لأن HTML ليس لديها مفهوم صيغة يقيّمها متصفح أو معالج نصوص
التحقق من اللصق، ومعالجة حافظة مشغولة
عادتان تلتقطان معظم مشكلات الحافظة قبل أن يفعل ذلك عميل. الصق في Notepad أولًا للتأكد من أن احتياطي CF_UNICODETEXT نص سليم مفصول بعلامات جدولة، ثم الصق النسخة نفسها في Word أو متصفح للتأكد من ظهور النسخة المنسّقة — حمولة تبدو صحيحة في واحد وخاطئة في الآخر تعني عادة أن علامات الجزء وقعت في المكان الخاطئ. ثم عامل النتيجة المنطقية الثنائية التي تُعيدها CopyToClipboard كذات معنى، لا زخرفية: يمكن لـOpenClipboard أن تفشل عندما تحمل عملية أخرى الحافظة مفتوحة، شائع بما يكفي على سطح مكتب مشغول بحيث ينتهي استدعاء واحد غير مُتحقَّق منه إلى عدم لصق شيء دون خطأ يفسر السبب، وهو ما تحرس منه إعادة المحاولة أدناه
function TryCopyRangeToClipboard(Workbook: TXLSXWorkbook): Boolean;
var
Attempt: Integer;
begin
Result := False;
for Attempt := 1 to 5 do
begin
Result := Workbook.Sheets[1].Range['A1:F40'].CopyToClipboard;
if Result then
Break;
Sleep(50); // give whichever app is holding the clipboard a moment
end;
if not Result then
raise Exception.Create('Could not take ownership of the clipboard');
end;
الصيغة نفسها ليست غريبة بمجرد أن تكون الترويسة دقيقة بالبايت واحتياطي النص العادي صادقًا بشأن ما يحتويه — فهي موجودة دون تغيير يُذكر منذ أن عرّفها Internet Explorer لأول مرة، ولا يزال كل تطبيق ويندوز رئيسي يقرأها بالطريقة نفسها. تقف CopyToClipboard إلى جانب PasteFromClipboard، جانب القراءة من التبادل نفسه، في سطح الحافظة والتصدير الأوسع الموثَّق في صفحة منتج مكوّن HotXLS