مقاله فنی

رکوردهای Selection و اسکرول پن در BIFF8 با HotXLS

HotXLS انتخاب‌های کاربرگ و موقعیت‌های اسکرول را به‌ازای هر پن، از طریق یک API پن-آگاه مشترک روی هر دو TXLSWorksheet و TXLSXWorksheet ذخیره می‌کند: SelectAreas و GetSelectedAreas و ScrollWindow و TryGetWindowScroll. برای فایل‌های .xls کلاسیک، HotXLS رکوردهای Selection (0x001D) مربوط به BIFF8 را حداکثر با 1369 ناحیه در هر رکورد می‌نویسد، نام‌های منطقی پن را به بایت‌های پنی که فرمت تعریف کرده تبدیل می‌کند، و هر محور اسکرول را روی رکورد Window2 یا Pane همان‌جا که Excel انتظارش را دارد نگه می‌دارد

مشکل معمولاً در یک ابزار مغایرت‌گیری یا ممیزی خودش را نشان می‌دهد. ابزار یک export دفترکل را باز می‌کند، هر سلولی را که با سیستم مبدأ نمی‌خواند پیدا می‌کند، و ورک‌بوک را با همان سلول‌ها که از قبل زیر یک ردیف هدر فریزشده انتخاب شده‌اند ذخیره می‌کند تا بازبین تفاوت‌ها را خودش پشت سر هم ببیند نه اینکه برایشان اسکرول کند. با چهل تفاوت خوب کار می‌کند. فایل آخر ماه 3000 تا دارد، و یک رکورد Selection تکی با 3000 ناحیه اصلاً نمی‌تواند وجود داشته باشد: بدنه‌اش به 18009 بایت نیاز دارد، بیش از دو برابر چیزی که یک رکورد BIFF8 می‌تواند حمل کند. موقعیت اسکرول هم تلهٔ مشابهی دارد. روی یک sheet با پن‌های فریزشده، «جایی که کاربر نگاه می‌کرد» چهار پن است که دو موقعیت ردیفی و دو موقعیت ستونی را به اشتراک می‌گذارند، نه یک مختصات تکی

چرا یک انتخاب بزرگ به بیش از یک رکورد Selection نیاز دارد؟

یک انتخاب بزرگ به چند رکورد نیاز دارد چون بدنهٔ رکورد BIFF8 سقف 8224 بایت دارد و هر ناحیهٔ انتخاب‌شده شش بایت ثابت هزینه دارد. [MS-XLS] §2.4.248 رکورد Selection را به‌صورت یک بخش ثابت 9 بایتی (بایت پن، rwAct و colAct برای سلول فعال، irefAct برای ناحیهٔ فعال، و cref برای تعداد ناحیه‌ها) به‌علاوهٔ cref ساختار RefU توصیف می‌کند که هر کدام دو ردیف 16 بیتی و دو ستون 8 بیتی حمل می‌کنند. بزرگ‌ترین تعدادی که جا می‌شود (8224 منهای 9) تقسیم بر 6 با گرد شدن به پایین است، یعنی 1369، که بدنهٔ 8223 بایتی می‌دهد، یک بایت زیر سقف. TXLSWorksheet.StoreSelectionGroup همین ثابت را به‌عنوان MaxAreasPerRecord استفاده می‌کند و یک گروه بزرگ‌تر را به‌صورت رکوردهای Selection پشت سر هم برای همان پن می‌نویسد، هر بار 1369 ناحیه

جزئیاتی که گاز می‌گیرد irefAct است. هر chunk همان ردیف فعال و ستون فعال و اندیس ناحیهٔ فعال را تکرار می‌کند، و irefAct به دنبالهٔ تجمیعی همهٔ chunkها اندیس می‌دهد، نه به ناحیه‌های داخل رکوردی که حملش می‌کند. انتخابی که یک ناحیه از سقف جلوتر است این را عینی می‌کند: 1370 ناحیه که آخری فعال است دو رکورد می‌شود، اولی با cref برابر 1369 و دومی با cref برابر 1، و هر دو irefAct برابر 1369 حمل می‌کنند. این مقدار از تعداد ناحیهٔ خود رکورد دوم بزرگ‌تر است. reader‌ای که irefAct را با cref همان رکورد چک می‌کند یک فایل معتبر را رد می‌کند، و reader‌ای که state خودش را با هر رکورد عوض می‌کند 1369 ناحیهٔ اول را گم می‌کند. reader مربوط به HotXLS رکوردهای پشت‌سرهمِ هم‌پن را در یک گروه append می‌کند، از هر chunk می‌خواهد روی سلول فعال و اندیس توافق داشته باشند، و چک بازه را فقط در رکورد EOF کاربرگ اجرا می‌کند، وقتی دنبالهٔ کامل معلوم شده. پس overload پن-اولِ SelectAreas هیچ سقف 1369 ناحیه‌ای ندارد. قبل از گرفتن قفل نوشتن کاربرگ هر ارجاع A1 و اندیس فعال را اعتبارسنجی می‌کند، و اگر چیزی بدشکل باشد False برمی‌گرداند و انتخاب قبلی دست‌نخورده می‌ماند

چرا HotXLS یک انتخاب بزرگ کاربرگ را به چند رکورد Selection در BIFF8 می‌نویسد: سقف بدنهٔ 8224 بایتی 9 بایت ثابت به‌علاوهٔ 1369 ناحیهٔ شش‌بایتی RefU را جا می‌دهد، پس 3000 ناحیه سه رکورد هم‌پن 1369 و 1369 و 262 می‌شود، و irefAct به دنبالهٔ تجمیعی اندیس می‌دهد بنابراین 1370 ناحیه با آخری فعال به هر دو رکورد irefAct 1369 می‌دهد
هر chunk همان سلول فعال و اندیس را تکرار می‌کند، reader مربوط به HotXLS رکوردهای پشت‌سرهمِ هم‌پن را در یک گروه append می‌کند، و چک بازه فقط در رکورد EOF و وقتی دنبالهٔ کامل معلوم شده اجرا می‌شود
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // ردیف هدر و ستون A سر جایشان می‌مانند

    SetLength(Diffs, 3000);
    for I := 0 to High(Diffs) do
      Diffs[I] := Format('C%d', [I + 2]);

    // فریز کردن انتخاب ذخیره‌شده را reset می‌کند، پس بعد از فریز انتخاب کنید.
    // 3000 ناحیه به‌صورت سه رکورد Selection ذخیره می‌شود: 1369 + 1369 + 262
    if not Sheet.SelectAreas(xlspBottomRight, Diffs, 0) then
      raise Exception.Create('Selection rejected');

    Book.SaveAs('reconciliation.xls');
  finally
    Book.Free;
  end;
end;

هر رکورد Selection از کدام بایت پن استفاده می‌کند؟

یک رکورد Selection پن خودش را با کد عددی‌ای که فرمت تعریف کرده مشخص می‌کند: 0 برای پایین-راست، 1 برای بالا-راست، 2 برای پایین-چپ، و 3 برای بالا-چپ. enumeration عمومی TXLSPanePosition به ترتیب خواندن تعریف شده، یعنی xlspTopLeft و xlspTopRight و xlspBottomLeft و xlspBottomRight، پس Ord(xlspTopLeft) برابر 0 است که همان پن پایین-راست در فایل است. cast کردن مستقیم enum به بایت پن هر انتخاب بالا-چپ را بدون هیچ خطایی روی پن پایین-راست می‌نویسد. هر نقطهٔ ورود پن-آگاه HotXLS این enum را به‌جای آن از طریق یک case صریح تبدیل می‌کند، تا فراخوان‌ها هرگز با کدهای عددی درگیر نشوند. وجود پن هم چک می‌شود: پن بالا-راست فقط با split عمودی وجود دارد، پایین-چپ فقط با split افقی، و پایین-راست فقط با هر دو. برای پنی که هندسهٔ فعلی split یا فریز ندارد، SelectAreas مقدار False برمی‌گرداند و GetSelectedAreas یک آرایهٔ خالی با ActiveAreaIndex برابر -1 برمی‌گرداند، بدون اینکه پن یا آبجکت انتخاب یا سلولی در ورک‌بوک بسازد

HotXLS چطور TXLSPanePosition را روی بایت پن Selection در BIFF8 نگاشت می‌کند: enum به ترتیب خواندن تعریف شده پس Ord(xlspTopLeft) برابر 0 است، در حالی که فایل 0 را برای پایین-راست و 1 را برای بالا-راست و 2 را برای پایین-چپ و 3 را برای بالا-چپ تعریف می‌کند، بنابراین هر نقطهٔ ورود پن-آگاه از طریق یک case صریح تبدیل می‌کند
cast کردن مستقیم enum به بایت پن هر انتخاب بالا-چپ را روی پن پایین-راست می‌نویسد، پس HotXLS قبل از نوشتن وجود پن را هم با هندسهٔ فعلی split یا فریز چک می‌کند

موقعیت اسکرول هر پن کجا زندگی می‌کند؟

موقعیت اسکرول هر پن بین دو رکورد پخش شده، چون چهار پن فقط دو موقعیت ردیفی و دو موقعیت ستونی را به اشتراک می‌گذارند. در یک ورک‌بوک کلاسیک، اولین ردیف دیدنی پن‌های بالایی و اولین ستون دیدنی پن‌های چپی Window2.rwTop و Window2.colLeft هستند، و ردیف پن‌های پایینی و ستون پن‌های راستی Pane.rwTop و Pane.colLeft. پس ScrollWindow(xlspTopRight, R, C) مقدار Window2.rwTop و Pane.colLeft را می‌نویسد، و ست کردن ستون پن بالا-راست پن پایین-راست را هم جابه‌جا می‌کند، درست مثل اینکه این دو در Excel یک اسکرول‌بار افقی مشترک دارند. متدهای عمومی از شمارهٔ ردیف و ستون 1-محور استفاده می‌کنند. پن ناموجود مقدار False برمی‌گرداند و هر دو خروجی کوئری را صفر می‌کند، و مختصات بیرون از بازه قبل از اینکه هر محوری عوض شود رد می‌شود. هیچ‌جای این‌ها به نحوهٔ رنگ کردن گرید توسط viewer وابسته نیست. یک کنترل رندر TopRow و LeftCol خودش را نگه می‌دارد، همان‌طور که مقالهٔ رندر کردن ورک‌بوک‌ها در یک گرید VCL سفارشی توضیح می‌دهد، و آن‌ها state زمان اجرا هستند، نه چیزی که ذخیره می‌شود

محورهای اسکرول هر پن HotXLS کجا زندگی می‌کنند: چهار پن دو موقعیت ردیفی و دو موقعیت ستونی را به اشتراک می‌گذارند، پس ردیف بالایی و ستون چپی Window2.rwTop و Window2.colLeft هستند و ردیف پایینی و ستون راستی Pane.rwTop و Pane.colLeft، و ScrollWindow(xlspTopRight, 1, 6) یک فیلد Window2 به‌علاوهٔ یک فیلد Pane می‌نویسد تا پایین-راست هم دنبال کند
XLSX همان داده را روی اتریبیوت‌های sheetView و pane topLeftCell پخش می‌کند، و فرو ریختن این دو لایه در یکی دقیقاً همان راهی است که یک موقعیت اسکرول بالایی یا چپی موقع لود بی‌سروصدا گم می‌شود

XLSX همان داده را روی دو المان پخش می‌کند: sheetView/@topLeftCell (ECMA-376 Part 1، §18.3.1.87) برای پنجره در کل و فرزند pane/@topLeftCell (§18.3.1.66) برای سمت پایین-راست یک split. هر دو اتریبیوت می‌توانند هم‌زمان حاضر باشند. HotXLS اول اتریبیوت بیرونی را در فیلدهای سطح پنجره می‌خواند، اجازه می‌دهد فرزند pane فقط فیلدهای سطح پن را override کند، و هر دو را جداگانه برمی‌گرداند. فرو ریختن دو لایه در یکی دقیقاً همان راهی است که یک موقعیت اسکرول بالایی یا چپی موقع لود بی‌سروصدا گم می‌شود. کپی‌های کاربرگ هر دو لایه را در هر دو موتور حمل می‌کنند. نقاط ورود قدیمی‌تر رفتار اصلی‌شان را دارند: propertyهای کلاسیک ScrollRow و ScrollColumn، و SetPaneScroll و GetPaneScroll مربوط به XLSX با اندیس صفر. خودِ هندسهٔ فریز و split با تنظیمات سطح sheet پیکربندی می‌شود که در محافظت از sheet، page setup و چاپ پوشش داده شده

var
  Row, Col: Integer;
begin
  Sheet.FreezePanes(1, 1);

  // پایین-راست: محور ردیف پایینی (Pane.rwTop) و محور ستون راست (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // بالا-راست محور ستون راست را به اشتراک می‌گذارد، پس این هم پایین-راست را به ستون 6 می‌برد
  Sheet.ScrollWindow(xlspTopRight, 1, 6);

  if Sheet.TryGetWindowScroll(xlspBottomRight, Row, Col) then
    Memo1.Lines.Add(Format('Bottom-right starts at row %d, column %d', [Row, Col]));
    // پایین-راست از ردیف 500 و ستون 6 شروع می‌شود
end;

وقتی یک رکورد Selection خراب است چه اتفاقی می‌افتد؟

وقتی یک رکورد Selection خراب است، HotXLS آن را به‌صورت بایت‌های opaque نگه می‌دارد، کد تشخیصی 1304 را گزارش می‌کند (xlsDiagnosticSelectionRecordInvalid)، و موقع ذخیره بدنهٔ اصلی را بایت‌به‌بایت برمی‌گرداند. قبل از اینکه یک رکورد به گروه پنش بپیوندد، reader به ترتیب آن را چک می‌کند. بایت پن باید 3 یا کمتر باشد. رکوردهای یک پن باید در stream پشت سر هم باشند. 9 بایت ثابت باید حاضر باشند. cref باید بین 1 و 1369 باشد، و بدنه باید دقیقاً 9 + cref × 6 بایت باشد. هر chunk گروه باید روی سلول فعال و irefAct توافق داشته باشد، بیت علامت irefAct نباید ست باشد، ستون فعال باید روی گرید باشد، و هیچ ناحیه‌ای نباید مرزهای برعکس داشته باشد. مشکلات یک رکورد فیزیکی تکی به‌ازای هر رکورد یک‌بار گزارش می‌شوند. تناقض‌هایی که فقط بعد از تجمیع ظاهر می‌شوند، مثل irefAct‌ای که از تعداد کل ناحیه‌ها جلوتر می‌رود یا سلول فعالی بیرون از ناحیهٔ اندیسی، به‌ازای هر گروه در EOF یک‌بار گزارش می‌شوند. یک گروه نامعتبر برای API تایپ‌دار نامرئی می‌ماند: GetSelectedAreas برای آن پن آرایهٔ خالی با اندیس -1 برمی‌گرداند، در حالی که هر پن دیگر سر کارش است

var
  I: Integer;
  D: TXLSDiagnostic;
begin
  if Book.Open('supplier-upload.xls') <> 1 then
    Exit;
  for I := 0 to Book.Diagnostics.Count - 1 do
  begin
    D := Book.Diagnostics[I];
    if D.Code = xlsDiagnosticSelectionRecordInvalid then
      Log.Add(Format('%s: record $%.4x kept opaque (%s)',
        [D.SheetName, D.RecordId, D.Message]));
  end;
end;

انتخاب‌ها از insert کردن ردیف و ستون چطور جان سالم به در می‌برند؟

انتخاب‌ها از ویرایش‌های ساختاری جان سالم به در می‌برند، چون insert یا delete کردن ردیف یا ستون کامل هر گروه پن نمایش‌داده‌شده را در هر دو موتور کلاسیک و XLSX از طریق یک remapper مشترک بازنگاشت می‌کند. ناحیه‌های باقی‌مانده ترتیبشان را نگه می‌دارند و ناحیهٔ فعال هویتش را. اگر ناحیهٔ فعال حذف شود، اولین جانشینِ باقی‌مانده فعال می‌شود، و اگر چیزی دنبالش نباشد آخرین پیشینِ باقی‌مانده. اگر همهٔ ناحیه‌ها حذف شوند، گروه به یک سلول در مرز حذف فرو می‌ریزد، و سلول فعالی که دیگر داخل ناحیهٔ انتخابی نمی‌افتد به گوشهٔ بالا-چپ آن ناحیه می‌رود، تا اندیس و مختصات هرگز با هم تناقض نداشته باشند. سقف‌ها عامدانه‌اند. گروه‌های کلاسیک نامعتبر توسط remapper رد می‌شوند به‌جای اینکه به یک انتخاب از پیش ساخته‌شده بازنویسی شوند، پس بایت‌های اصلی‌شان همچنان round trip می‌کنند. ویرایش یک پن فقط رکوردهای همان پن را عوض می‌کند و بقیه را byte-identical می‌گذارد. ODS اصلاً هیچ state انتخاب پن نمی‌گیرد، چون ODF هیچ ساختار view کاربرگ معادلی برای حملش ندارد

اگر اپلیکیشن شما فایل‌های .xls می‌نویسد که کاربرها بازشان می‌کنند و باید درشان حرکت کنند، چه برای بازبینی سلول‌های علامت‌خورده، چه برای ادامه از جایی که رها کرده‌اند، چه برای به اشتراک گذاشتن یک dashboard فریزشده، API انتخاب و اسکرول پن-آگاه بخشی از کامپوننت spreadsheet Delphi HotXLS است و برای XLS و XLSX به یک شکل کار می‌کند