Microsoft Excelの行の高さの動作方法

時々私は退屈し、デバッガを装備して、さまざまなプログラムを掘り始めます。 今回は、Excelでの選択が決まり、行の高さを処理する方法、格納するもの、セル範囲の高さをどのように考慮するかなどを把握したいという要望がありました。 Excel 2010(excel.exe、32ビット、バージョン14.0.4756.1000、SHA1 a805cf60a5542f21001b0ea5d142d1cd0ee00b28)を解析しました。


理論から始めましょう


Microsoft OfficeのVBAドキュメントに目を向けると、2つのプロパティを通じて行の高さを取得できることがわかります。



また、こちらもご覧ください: Excelの仕様と制限 。 行の最大の高さは409ポイントであることがわかります。 残念ながら、これはMicrosoftの公式ドキュメントが少し巧妙な場合だけではありません。 実際、Excelコードでは、最大行の高さは2047ピクセルに設定されており、これは1535.25ポイントです。 また、最大フォントサイズは409.55ポイントです。 VBA / Interopで単純に割り当てることで、このような巨大な高さの行を取得することはできませんが、行を取得し、Cambria Mathフォントを最初のセルに設定し、フォントサイズを409.55ポイントに設定できます。 その後、Excelはそのcなアルゴリズムを使用して、セル形式に基づいて行の高さを計算し、2047ピクセルを超える数を取得し(単語を信じる)、行を可能な最大の高さに設定します。 UIからこのシリーズの高さを求めると、Excelは高さが409.5ポイントであると嘘をつきますが、VBAを介してシリーズの高さを要求すると、正直な1535.25ポイント、つまり2047ピクセルになります。 確かに、ドキュメントを保存した後でも、高さは409.5ポイントまで低下します。 この操作は、次のビデオで確認できます。http//recordit.co/ivnFEsELLI


前の段落でピクセルを知っていました。 Excelは、実際にセルのサイズを整数で格納および計算します(通常、可能な限りすべてを整数で処理します)。 ほとんどの場合、これらはピクセルに何らかの係数を掛けたものです。 興味深いことに、Excelは外観のスケールを通常の分数の形式で保存します。たとえば、75%スケールは2つの数字3と4として保存されます。しかし、彼はこの操作を最後にすでに実行しているため、すべてが小数で考慮されるという効果が生まれます。 これを確認するには、VBAで次のコードを記述します。


w.Rows(1).RowHeight = 75.375 Debug.Print w.Rows(1).Height 

VBAは75を配ります 75.375ピクセルは100.5ピクセルになり、Excelはそれを購入する余裕がなく、小数部分を最大100ピクセルに落とします。 VBAがポイント単位で行の高さを要求すると、Excelは正直に100ピクセルをポイントに変換し、75を返します。


原則として、シリーズの高さに関する情報を記述するクラスをC#で記述することにすでに着手しています。


 class RowHeightInfo { public ushort Value { get; set; } //    ,   4. public ushort Flags { get; set; } //  } 

とりあえず私の言葉を使わなければなりませんが、Excelでは行の高さはそのまま保存されます。 つまり、行の高さが75ポイント、ピクセル単位で100になるように指定されている場合、400がValueに格納されます。Flagsのすべてのビットの意味を完全には把握していません(フラグの値を把握するのは難しく、長いです)その0x4000は高さが手動で設定されている行に対して設定され、0x2000は非表示の行に対して設定されています。 一般に、手動で高さを設定した表示行の場合、Flagsはほとんどの場合0x4005に等しく、高さはFlagsフォーマットに基づいて計算される行の場合は0xAまたは0x800Eのいずれかです。


列の高さを尋ねる


原則として、excel.exeのメソッドを見ることができます。このメソッドは、インデックスによって行の高さを返します(美しいコードを提供してくれたHexRaysに感謝します)。


 int __userpurge GetRowHeight@<eax>(signed int rowIndex@<edx>, SheetLayoutInfo *sheetLayoutInfo@<esi>, bool flag) { RowHeightInfo *rowHeightInfo; // eax int result; // ecx if ( sheetLayoutInfo->dword1A0 ) return sheetLayoutInfo->defaultFullRowHeightMul4 | (~(sheetLayoutInfo->defaultRowDelta2 >> 14 << 15) & 0x8000); if ( rowIndex < sheetLayoutInfo->MinRowIndexNonDefault ) return sheetLayoutInfo->defaultFullRowHeightMul4 | (~(sheetLayoutInfo->defaultRowDelta2 >> 14 << 15) & 0x8000); if ( rowIndex >= sheetLayoutInfo->MaxRowIndexNonDefault ) return sheetLayoutInfo->defaultFullRowHeightMul4 | (~(sheetLayoutInfo->defaultRowDelta2 >> 14 << 15) & 0x8000); rowHeightInfo = GetRowHeightCore(sheetLayoutInfo, rowIndex); if ( !rowHeightInfo ) return sheetLayoutInfo->defaultFullRowHeightMul4 | (~(sheetLayoutInfo->defaultRowDelta2 >> 14 << 15) & 0x8000); result = 0; if ( flag || !(rowHeightInfo->Flags & 0x2000) ) result = rowHeightInfo->Value; if ( !(rowHeightInfo->Flags & 0x4000) ) result |= 0x8000u; return result; } 

dword1A0とは何なのか、まだわかりません。 このフラグが設定されている場所が見つかりませんでした:(
私にとってもdefaultRowDelta2とは何なのか、いまだに謎のままです。 Excelがフォーマットに基づいて行の高さを計算する場合、2つの数値の合計として表されます。 defaultRowDelta2は、標準の行の高さのこの合計からの2番目の数値です。 フラグパラメーターの値も不思議です。 falseで渡されたフラグでこのメソッドの呼び出しを見た場所
SheetLayoutInfoクラスもこのメソッドに表示されます。 シートの外観に関するあらゆる種類の情報が多数保存されているため、このように名前を付けました。 SheetLayoutInfoには、次のようなフィールドがあります。



原則として、このメソッドのロジックは理解できます。


  1. シリーズのインデックスが非標準の高さを持つ最初のインデックスよりも小さい場合、標準を返します。
  2. シリーズのインデックスが非標準の高さを持つ最後のインデックスよりも大きい場合、標準を返します。
  3. それ以外の場合は、GetRowHeightCoreメソッドからシリーズのrowHeightInfoオブジェクトを取得します。
  4. rowHeightInfo == nullの場合、標準の行の高さを返します。
  5. フラグには魔法がありますが、一般に、rowHeightInfo.Valueにあるものを返し、行の高さが手動で設定されていない場合、応答の16番目のビットを設定します。

このコードをC#で書き換えると、次のようになります。


 const ulong HiddenRowMask = 0x2000; const ulong CustomHeightMask = 0x4000; const ushort DefaultHeightMask = 0x8000; public static ushort GetRowHeight(int rowIndex, SheetLayoutInfo sheetLayoutInfo) { ushort defaultHeight = (ushort) (sheetLayoutInfo.DefaultFullRowHeightMul4 | (~(sheetLayoutInfo.DefaultRowDelta2 >> 14 << 15) & DefaultHeightMask)); if (rowIndex < sheetLayoutInfo.MinRowIndexNonDefault) return defaultHeight; if (rowIndex >= sheetLayoutInfo.MaxRowIndexNonDefault) return defaultHeight; RowHeightInfo rowHeightInfo = GetRowHeightCore(sheetLayoutInfo, rowIndex); if (rowHeightInfo == null) return defaultHeight; ushort result = 0; if ((rowHeightInfo.Flags & HiddenRowMask) == 0) result = rowHeightInfo.Value; if ((rowHeightInfo.Flags & CustomHeightMask) == 0) result |= DefaultHeightMask; return result; } 

これで、GetRowHeightCore内で何が起こっているのかを確認できます。


 RowHeightInfo *__fastcall GetRowHeightCore(SheetLayoutInfo *sheetLayoutInfo, signed int rowIndex) { RowHeightInfo *result; // eax RowsGroupInfo *rowsGroupInfo; // ecx int rowInfoIndex; // edx result = 0; if ( rowIndex < sheetLayoutInfo->MinRowIndexNonDefault || rowIndex >= sheetLayoutInfo->MaxRowIndexNonDefault ) return result; rowsGroupInfo = sheetLayoutInfo->RowsGroups[sheetLayoutInfo-GroupIndexDelta + (rowIndex >> 4)]; result = 0; if ( !rowsGroupInfo ) return result; rowInfoIndex = rowsGroupInfo->Indices[rowIndex & 0xF]; if ( rowInfoIndex ) result = (rowsGroupInfo + 8 * (rowInfoIndex + rowsGroupInfo->wordBA + rowsGroupInfo->wordBC - rowsGroupInfo->wordB8)); return result; } 

  1. 繰り返しますが、最初に、Excelは行インデックスが変更された高さを持つ行にあるかどうかを確認し、そうでない場合はnullを返します。
  2. 目的の行のグループを検索します;そのようなグループがない場合、nullを返します。
  3. グループ内のシリーズのインデックスを取得します。
  4. 次に、シリーズのインデックスによって、RowHeightInfoクラスの目的のオブジェクトを見つけます。 wordBA、wordBC、wordB8-いくつかの定数。 それらは歴史と共にのみ変化します。 原則として、それらはアルゴリズムの理解に影響しません。

ここでは、トピックから逸​​脱して、RowsGroupInfoについて詳しく説明する価値があります。 ExcelはRowHeightInfoを16個のグループに保存します。RowsGroupInfoクラスで表されるi番目のグループは、i×16からi×16 + 15までの行に関する情報を保存します。


ただし、RowsGroupInfoの行の高さ情報は、やや珍しい方法で保存されます。 最も可能性が高いのは、Excelで履歴を保持する必要があるためです。


RowsGroupInfoには3つの重要なフィールドがあります。Indices、HeightInfos、およびRowsCountです。2番目は上記のコードでは表示されません(この行にあるはずです:(rowsGroupInfo + 8×(...))。rowInfoIndexは異なる値を取ることができるため、1000を超えるものを見てきましたが、IDAでそのような構造を設定する方法がわかりません。RowsCountフィールドは上記のコードには表示されませんが、これは実際に非標準の行がグループに格納される数です。
さらに、SheetLayoutInfoにはGroupIndexDeltaがあります。これは、グループの実際のインデックスと、行の高さが変更された最初のグループのインデックスとの差です。


Indicesフィールドには、グループ内のシリーズの各インデックスのRowHeightInfoオフセットが格納されます。 それらは順番に格納されますが、HeightInfosではRowHeightInfoはすでに変更の順序で格納されています。


新しい空白のシートがあり、行番号23の高さを何らかの方法で変更したと仮定します。この行は16行の2番目のグループにあり、次のようになります。


  1. Excelは、このシリーズのグループインデックスを決定します。 現在の場合、インデックスは1になり、GroupIndexDelta = -1に変更されます。
  2. 一連の行のRowsGroupInfoクラスのオブジェクトを作成し、それをsheetLayoutInfo-> RowsGroupsのインデックス0(sheetLayoutInfo-> GroupIndexDelta + 1)の下に配置します。
  3. RowsGroupInfoでは、ExcelはRowsCount、wordBA、wordBC、wordB8などの16個の4バイトインデックスにメモリを割り当てます。
  4. 次に、Excelはビット単位のAND演算を使用して、グループ内のシリーズのインデックスを計算します(これは、除算の残りを取得するよりもはるかに高速です):rowIndex&0xF。 グループ内の目的のインデックスは次のようになります。23&0xF = 7;
  5. その後、Excelはインデックス7のオフセットを取得します:offset = Indices [7]。 offset = 0の場合、ExcelはRowsGroupIntoの最後に8バイトを割り当て、RowsCountを1増やし、新しいオフセットをインデックス[7]に書き込みます。 いずれの場合でも、最後に、Excelは新しい行の高さとフラグに関する情報をRowsGroupInfoにオフセットで書き込みます。

Cows RowsGroupInfoクラス自体は次のようになります。


 class RowsGroupInfo { public int[] Indices { get; } public List<RowHeightInfo> HeightInfos { get; } public RowsGroupInfo() { Indices = new int[SheetLayoutInfo.MaxRowsCountInGroup]; HeightInfos = new List<RowHeightInfo>(); for (int i = 0; i < SheetLayoutInfo.MaxRowsCountInGroup; i++) { Indices[i] = -1; } } } 

GetRowHeightCoreメソッドは次のようになります。


 static RowHeightInfo GetRowHeightCore(SheetLayoutInfo sheetLayoutInfo, int rowIndex) { if (rowIndex < sheetLayoutInfo.MinRowIndexNonDefault || rowIndex >= sheetLayoutInfo.MaxRowIndexNonDefault) return null; RowsGroupInfo rowsGroupInfo = sheetLayoutInfo.RowsGroups[sheetLayoutInfo.GroupIndexDelta + (rowIndex >> 4)]; if (rowsGroupInfo == null) return null; int rowInfoIndex = rowsGroupInfo.Indices[rowIndex & 0xF]; return rowInfoIndex != -1 ? rowsGroupInfo.HeightInfos[rowInfoIndex] : null; } 

これがSetRowHeightの外観です(excel.exeからコードをリストしませんでした)。


 public static void SetRowHeight(int rowIndex, ushort newRowHeight, ushort flags, SheetLayoutInfo sheetLayoutInfo) { sheetLayoutInfo.MaxRowIndexNonDefault = Math.Max(sheetLayoutInfo.MaxRowIndexNonDefault, rowIndex + 1); sheetLayoutInfo.MinRowIndexNonDefault = Math.Min(sheetLayoutInfo.MinRowIndexNonDefault, rowIndex); int realGroupIndex = rowIndex >> 4; if (sheetLayoutInfo.RowsGroups.Count == 0) { sheetLayoutInfo.RowsGroups.Add(null); sheetLayoutInfo.GroupIndexDelta = -realGroupIndex; } else if (sheetLayoutInfo.GroupIndexDelta + realGroupIndex < 0) { int bucketSize = -(sheetLayoutInfo.GroupIndexDelta + realGroupIndex); sheetLayoutInfo.RowsGroups.InsertRange(0, new RowsGroupInfo[bucketSize]); sheetLayoutInfo.GroupIndexDelta = -realGroupIndex; } else if (sheetLayoutInfo.GroupIndexDelta + realGroupIndex >= sheetLayoutInfo.RowsGroups.Count) { int bucketSize = sheetLayoutInfo.GroupIndexDelta + realGroupIndex - sheetLayoutInfo.RowsGroups.Count + 1; sheetLayoutInfo.RowsGroups.AddRange(new RowsGroupInfo[bucketSize]); } RowsGroupInfo rowsGroupInfo = sheetLayoutInfo.RowsGroups[sheetLayoutInfo.GroupIndexDelta + realGroupIndex]; if (rowsGroupInfo == null) { rowsGroupInfo = new RowsGroupInfo(); sheetLayoutInfo.RowsGroups[sheetLayoutInfo.GroupIndexDelta + realGroupIndex] = rowsGroupInfo; } int rowInfoIndex = rowsGroupInfo.Indices[rowIndex & 0xF]; RowHeightInfo rowHeightInfo; if (rowInfoIndex == -1) { rowHeightInfo = new RowHeightInfo(); rowsGroupInfo.HeightInfos.Add(rowHeightInfo); rowsGroupInfo.Indices[rowIndex & 0xF] = rowsGroupInfo.HeightInfos.Count - 1; } else { rowHeightInfo = rowsGroupInfo.HeightInfos[rowInfoIndex]; } rowHeightInfo.Value = newRowHeight; rowHeightInfo.Flags = flags; } 

ちょっとした練習


上記の例の後、行23の高さを変更すると、Excelは次のようになります(行23の高さを75ポイントに設定します)。


sheetLayoutInfo
  • DefaultFullRowHeightMul4 = 80
  • DefaultRowDelta2 = 5
  • MaxRowIndexNonDefault = 24
  • MinRowIndexNonDefault = 23
  • GroupIndexDelta = -1
  • RowsGroups Count = 1
    • [0] RowsGroupInfo
    • HeightInfosカウント= 1
      • [0] RowHeightInfo
      • フラグ= 0x4005
      • 値= 100
    • 指数
      • [0] = -1
      • [1] = -1
      • [2] = -1
      • [3] = -1
      • [4] = -1
      • [5] = -1
      • [6] = -1
      • [7] = 0
      • [8] = -1
      • [9] = -1
      • [10] = -1
      • [11] = -1
      • [12] = -1
      • [13] = -1
      • [14] = -1
      • [15] = -1

ここと次の例では、メモリからの直接ダンプはあまり有益ではないため、Visual Studioで自己記述クラスから作成されたExcelメモリ内のデータの外観の概略図をレイアウトします。
23行目を非表示にしましょう。これを行うには、フラグの0x2000ビットを設定します。 メモリをライブに変更します。 結果はこのビデオで見ることができます: http : //recordit.co/79vYIbwbzB
行を非表示にするたびに、Excelは同じことを行います。
次に、行の高さを明示的にではなく、セル形式で設定しましょう。 セルA20のフォントの高さを40ポイントにすると、セルの高さはポイントで45.75になり、Excelメモリでは次のようになります。
sheetLayoutInfo
  • DefaultFullRowHeightMul4 = 80
  • DefaultRowDelta2 = 5
  • MaxRowIndexNonDefault = 24
  • MinRowIndexNonDefault = 20
  • GroupIndexDelta = -1
  • RowsGroups Count = 1
    • [0] RowsGroupInfo
    • HeightInfosカウント= 2
      • [0] RowHeightInfo
      • フラグ= 0x4005
      • 値= 100
      • [1] RowHeightInfo
      • フラグ= 0x800E
      • 値= 244
    • 指数
      • [0] = -1
      • [1] = -1
      • [2] = -1
      • [3] = -1
      • [4] = 1
      • [5] = -1
      • [6] = -1
      • [7] = 0
      • [8] = -1
      • [9] = -1
      • [10] = -1
      • [11] = -1
      • [12] = -1
      • [13] = -1
      • [14] = -1
      • [15] = -1

行の高さが標準ではない場合、Excelは常に行の高さを保存することに気付くかもしれません。 高さが明示的に設定されていなくても、セルまたは書式の内容に基づいて計算されている場合でも、Excelはそれを一度計算し、結果を適切なグループに入れます。


行の挿入/削除を処理します


行の挿入/削除時に何が起こるかを解析することは興味深いでしょう。 excel.exeの対応するコードを見つけるのは難しくありませんが、逆アセンブルする必要はありませんでした。その一部をご覧ください。


sub_305EC930

フラグa5は、現在行われている操作を決定します。


 int __userpurge sub_305EC930@<eax>(int a1@<eax>, int a2@<edx>, int a3@<ecx>, int a4, int a5, int a6) { int v6; // esi int v7; // ebx int v8; // edi int v9; // edx int v10; // ecx size_t v11; // eax _WORD *v12; // ebp size_t v13; // eax size_t v14; // eax int v15; // eax unsigned __int16 *v16; // ecx _WORD *v17; // eax _WORD *v18; // ecx int v19; // edx __int16 v20; // bx int v21; // eax _WORD *v22; // ecx int v24; // edx int v25; // eax int v26; // esi int v27; // ebx size_t v28; // eax int v29; // ebp size_t v30; // eax int v31; // esi size_t v32; // eax int v33; // eax unsigned __int16 *v34; // ecx int v35; // eax _WORD *v36; // edx _WORD *v37; // ecx int v38; // eax __int16 v39; // bx int v40; // eax _WORD *v41; // ecx int v42; // [esp+10h] [ebp-48h] int v43; // [esp+10h] [ebp-48h] int v44; // [esp+14h] [ebp-44h] char v45; // [esp+14h] [ebp-44h] int Dst[16]; // [esp+18h] [ebp-40h] int v47; // [esp+5Ch] [ebp+4h] int v48; // [esp+60h] [ebp+8h] v6 = a1; v7 = a1 & 0xF; v8 = a2; if ( !a5 ) { v24 = a4 - a1; v25 = a1 - a3; v43 = a4 - v6; if ( v7 >= v25 ) v7 = v25; v47 = a4 - v7; v26 = v6 - v7; v27 = v7 + 1; v48 = v27; if ( !v8 ) return v27; v28 = 4 * v24; if ( (4 * v24) > 0x40 ) v28 = 64; v45 = v27 + v26; v29 = (v27 + v26) & 0xF; memmove(Dst, (v8 + 4 * v29), v28); v30 = 4 * v27; if ( (4 * v27) > 0x40 ) v30 = 64; v31 = v26 & 0xF; memmove((v8 + 4 * (v47 & 0xF)), (v8 + 4 * v31), v30); v32 = 4 * v43; if ( (4 * v43) > 0x40 ) v32 = 64; memmove((v8 + 4 * v31), Dst, v32); if ( !a6 ) return v48; v33 = v29; if ( v29 < v29 + v43 ) { v34 = (v8 + 4 * v29 + 214); do { Dst[v33++] = *v34 >> 15; v34 += 2; } while ( v33 < v29 + v43 ); } v35 = (v45 - 1) & 0xF; if ( v35 >= v31 ) { v36 = (v8 + 4 * ((v27 + v47 - 1) & 0xF) + 214); v37 = (v8 + 4 * ((v45 - 1) & 0xF) + 214); v38 = v35 - v31 + 1; do { v39 = *v37 ^ (*v37 ^ *v36) & 0x7FFF; v37 -= 2; *v36 = v39; v36 -= 2; --v38; } while ( v38 ); v27 = v48; } v40 = v31; if ( v31 >= v31 + v43 ) return v27; v41 = (v8 + 4 * v31 + 214); do { *v41 = *v41 & 0x7FFF | (LOWORD(Dst[v40++]) << 15); v41 += 2; } while ( v40 < v31 + v43 ); return v27; } v9 = a1 - a4; v10 = a3 - a1; v42 = a1 - a4; v48 = 16 - v7; if ( 16 - v7 >= v10 ) v48 = v10; if ( !v8 ) return v48; v11 = 4 * v9; if ( (4 * v9) > 0x40 ) v11 = 64; v12 = (v8 + 4 * (a4 & 0xF)); v44 = a4 & 0xF; memmove(Dst, v12, v11); v13 = 4 * v48; if ( (4 * v48) > 0x40 ) v13 = 64; memmove(v12, (v8 + 4 * v7), v13); v14 = 4 * v42; if ( (4 * v42) > 0x40 ) v14 = 64; memmove((v8 + 4 * ((a4 + v48) & 0xF)), Dst, v14); if ( !a6 ) return v48; v15 = a4 & 0xF; if ( v44 < v44 + v42 ) { v16 = (v8 + 4 * v44 + 214); do { Dst[v15++] = *v16 >> 15; v16 += 2; } while ( v15 < v44 + v42 ); } if ( v7 < v48 + v7 ) { v17 = (v8 + 4 * v7 + 214); v18 = v12 + 107; v19 = v48; do { v20 = *v17 ^ (*v17 ^ *v18) & 0x7FFF; v17 += 2; *v18 = v20; v18 += 2; --v19; } while ( v19 ); } v21 = a4 & 0xF; if ( v44 >= v44 + v42 ) return v48; v22 = (v8 + 4 * (v44 + v48) + 214); do { *v22 = *v22 & 0x7FFF | (LOWORD(Dst[v21++]) << 15); v22 += 2; } while ( v21 < v44 + v42 ); return v48; } 

さらに、外観では、そこで何が起こっているかを大まかに理解し、残りを間接的な兆候で終了することができます。
これらの間接的な機能を特定しようとします。 まず、行16〜64の高さをランダムな順序で設定します。 次に、インデックス39の下の行の前に、新しい行を挿入します。 新しい行は行38の高さをコピーします。
シリーズを追加する前後のシリーズグループの情報を見てみましょう。大胆な違いを強調しました。


行を追加する前に行を追加した後
最初のグループの変位:最初のグループの変位:
0E、04、07、00、05、0A、09、0F、03、06、08、0D、01、0B、0C、020E、04、07、00、05、0A、09、0F、03、06、08、0D、01、0B、0C、02
最初のグループの行の高さの値:最初のグループの行の高さの値:
05、2B、35、45、4B、50、5B、6B、7B、8B、A5、AB、B0、B5、E0、10005、2B、35、45、4B、50、5B、6B、7B、8B、A5、AB、B0、B5、E0、100
2番目のグループの変位:2番目のグループの変位:
06、02、0E、09、01、07、0F、0C 、00、0A、04、0B、 03、08、0D、0506、02、0E、09、01、07、0F、05、0C、00、0A、04、0B、 03、08、0D
2番目のグループの行の高さの値:2番目のグループの行の高さの値:
10、15、20、25、30、75、85、90、9B、A0、C5、CB、D0、D5、E5、F010、15、20、25、30 、F0、85、90、9B、A0、C5、CB、D0、D5、E5、F0
3番目のグループの変位:3番目のグループの変位:
0C、08、0E、07、0A、01、06、0F、09、0D、00、05、0B、02、04、0303、0C、08、0E、07、0A、01、06、0F、09、0D、00、05、0B、02、04
3番目のグループの行の高さの値:3番目のグループの行の高さの値:
0B、1B、3B、40、55、60、65、70、80、95、BB、C0、DB、EB、F5、FB0B、1B、3B、75、55、60、65、70、80、95、BB、C0、DB、EB、F5、FB
4番目のグループのオフセット:4番目のグループのオフセット:
_00
4番目のグループの行の高さの値:4番目のグループの行の高さの値:
_40

これは予想されたものです。Excelは2番目のグループにインデックス7(39&0xF)の新しい行を挿入します。オフセットは0x05で、インデックス6の行の高さをコピーします。グループ、最後の行はそこから4番目にプッシュされます。


29行目を削除するとどうなるか見てみましょう。


行を削除する前に行を削除した後
最初のグループの変位:最初のグループの変位:
0E、 04、07、00、05、0A、09、0F、03、06、08、0D、01、0B、0C、020E、 04、07、00、05、0A、09、0F、03、06、08、0D、01、0C、02、0B
最初のグループの行の高さの値:最初のグループの行の高さの値:
05、2B、35、45、4B、50、5B、6B、7B、8B、A5、 AB 、B0、B5、E0、10005、2B、35、45、4B、50、5B、6B、7B、8B、A5、85、B0、B5、E0、100
2番目のグループの変位:2番目のグループの変位:
06、02、0E、09、01、07、0F、05、0C、00、0A、04、0B、03、08、0D02、0E、09、01、07、0F、05、0C、00、0A、04、0B、03、08、0D、 06
2番目のグループの行の高さの値:2番目のグループの行の高さの値:
10、15、20、25、30、F0、85、90、9B、A0、C5、CB、D0、D5、E5、F010、15、20、25、30、F0、75、90、9B、A0、C5、CB、D0、D5、E5、F0
3番目のグループの変位:3番目のグループの変位:
03、0C、08、0E、07、0A、01、06、0F、09、0D、00、05、0B、02、040C、08、0E、07、0A、01、06、0F、09、0D、00、05、0B、02、04、03
3番目のグループの行の高さの値:3番目のグループの行の高さの値:
0B、1B、3B、75、55、60、65、70、80、95、BB、C0、DB、EB、F5、FB0B、1B、3B、40、55、60、65、70、80、95、BB、C0、DB、EB、F5、FB
4番目のグループのオフセット:4番目のグループのオフセット:
0000
4番目のグループの行の高さの値:4番目のグループの行の高さの値:
4050

原則として、行が削除されると、挿入と反対の操作が発生します。 同時に、4番目のグループは存在し続け、そこにある行の高さの値は、対応するフラグ-0x8005を持つ標準の高さで埋められます。


このデータは、C#でこのアルゴリズムを再現するのに十分です。


挿入行
 public static void InsertRow(SheetLayoutInfo sheetLayoutInfo, int rowIndex) { if (rowIndex >= sheetLayoutInfo.MaxRowIndexNonDefault) return; RowHeightInfo etalonRowHeightInfo = GetRowHeightCore(sheetLayoutInfo, rowIndex); RowHeightInfo newRowHeightInfo = etalonRowHeightInfo != null ? etalonRowHeightInfo.Clone() : CreateDefaultRowHeight(sheetLayoutInfo); int realGroupIndex = (rowIndex + 1) >> 4; int newRowInGroupIndex = (rowIndex + 1) & 0xF; int groupIndex; for (groupIndex = realGroupIndex + sheetLayoutInfo.GroupIndexDelta; groupIndex < sheetLayoutInfo.RowsGroups.Count; groupIndex++, newRowInGroupIndex = 0) { if (groupIndex < 0) continue; if (groupIndex == SheetLayoutInfo.MaxGroupsCount) break; RowsGroupInfo rowsGroupInfo = sheetLayoutInfo.RowsGroups[groupIndex]; if (rowsGroupInfo == null) { if ((newRowHeightInfo.Flags & CustomHeightMask) == 0) continue; rowsGroupInfo = new RowsGroupInfo(); sheetLayoutInfo.RowsGroups[groupIndex] = rowsGroupInfo; } int rowInfoIndex = rowsGroupInfo.Indices[newRowInGroupIndex]; RowHeightInfo lastRowHeightInGroup; if (rowInfoIndex == -1 || rowsGroupInfo.HeightInfos.Count < SheetLayoutInfo.MaxRowsCountInGroup) { lastRowHeightInGroup = GetRowHeightCore(sheetLayoutInfo, ((groupIndex - sheetLayoutInfo.GroupIndexDelta) << 4) + SheetLayoutInfo.MaxRowsCountInGroup - 1); Array.Copy(rowsGroupInfo.Indices, newRowInGroupIndex, rowsGroupInfo.Indices, newRowInGroupIndex + 1, SheetLayoutInfo.MaxRowsCountInGroup - 1 - newRowInGroupIndex); rowsGroupInfo.HeightInfos.Add(newRowHeightInfo); rowsGroupInfo.Indices[newRowInGroupIndex] = rowsGroupInfo.HeightInfos.Count - 1; } else { int lastIndex = rowsGroupInfo.Indices[SheetLayoutInfo.MaxRowsCountInGroup - 1]; lastRowHeightInGroup = rowsGroupInfo.HeightInfos[lastIndex]; Array.Copy(rowsGroupInfo.Indices, newRowInGroupIndex, rowsGroupInfo.Indices, newRowInGroupIndex + 1, SheetLayoutInfo.MaxRowsCountInGroup - 1 - newRowInGroupIndex); rowsGroupInfo.HeightInfos[lastIndex] = newRowHeightInfo; rowsGroupInfo.Indices[newRowInGroupIndex] = lastIndex; } newRowHeightInfo = lastRowHeightInGroup ?? CreateDefaultRowHeight(sheetLayoutInfo); } if ((newRowHeightInfo.Flags & CustomHeightMask) != 0 && groupIndex != SheetLayoutInfo.MaxGroupsCount) { SetRowHeight(((groupIndex - sheetLayoutInfo.GroupIndexDelta) << 4) + newRowInGroupIndex, newRowHeightInfo.Value, newRowHeightInfo.Flags, sheetLayoutInfo); } else { sheetLayoutInfo.MaxRowIndexNonDefault = Math.Min(sheetLayoutInfo.MaxRowIndexNonDefault + 1, SheetLayoutInfo.MaxRowsCount); } } 

行を削除
 public static void RemoveRow(SheetLayoutInfo sheetLayoutInfo, int rowIndex) { if (rowIndex >= sheetLayoutInfo.MaxRowIndexNonDefault) return; int realGroupIndex = rowIndex >> 4; int newRowInGroupIndex = rowIndex & 0xF; int groupIndex; for (groupIndex = realGroupIndex + sheetLayoutInfo.GroupIndexDelta; groupIndex < sheetLayoutInfo.RowsGroups.Count; groupIndex++, newRowInGroupIndex = 0) { if (groupIndex < -1) continue; if (groupIndex == -1) { sheetLayoutInfo.RowsGroups.Insert(0, null); sheetLayoutInfo.GroupIndexDelta++; groupIndex = 0; } if (groupIndex == SheetLayoutInfo.MaxGroupsCount) break; var newRowHeightInfo = groupIndex == SheetLayoutInfo.MaxGroupsCount - 1 ? null : GetRowHeightCore(sheetLayoutInfo, (groupIndex - sheetLayoutInfo.GroupIndexDelta + 1) << 4); RowsGroupInfo rowsGroupInfo = sheetLayoutInfo.RowsGroups[groupIndex]; if (rowsGroupInfo == null) { if (newRowHeightInfo == null || (newRowHeightInfo.Flags & CustomHeightMask) == 0) continue; rowsGroupInfo = new RowsGroupInfo(); sheetLayoutInfo.RowsGroups[groupIndex] = rowsGroupInfo; } if (newRowHeightInfo == null) { newRowHeightInfo = CreateDefaultRowHeight(sheetLayoutInfo); } int rowInfoIndex = rowsGroupInfo.Indices[newRowInGroupIndex]; if (rowInfoIndex == -1) { for (int i = newRowInGroupIndex; i < SheetLayoutInfo.MaxRowsCountInGroup - 1; i++) { rowsGroupInfo.Indices[i] = rowsGroupInfo.Indices[i + 1]; } rowsGroupInfo.HeightInfos.Add(newRowHeightInfo); rowsGroupInfo.Indices[SheetLayoutInfo.MaxRowsCountInGroup - 1] = rowsGroupInfo.HeightInfos.Count - 1; } else { for(int i = newRowInGroupIndex; i < rowsGroupInfo.HeightInfos.Count - 1; i++) { rowsGroupInfo.Indices[i] = rowsGroupInfo.Indices[i + 1]; } rowsGroupInfo.Indices[rowsGroupInfo.HeightInfos.Count - 1] = rowInfoIndex; rowsGroupInfo.HeightInfos[rowInfoIndex] = newRowHeightInfo; } } if(rowIndex <= sheetLayoutInfo.MinRowIndexNonDefault) { sheetLayoutInfo.MinRowIndexNonDefault = Math.Max(sheetLayoutInfo.MinRowIndexNonDefault - 1, 0); } } 

上記のコードはすべてGitHubで見つけることができます


結論


Excelコードが興味深いトリックに驚いたのはこれが初めてではありません。 今回は、行の高さに関する情報を彼がどのように保存するかを知りました。 コミュニティに興味がある場合は、次の記事でExcelがセル範囲の高さをどのように考慮するかを示します(ネタバレ:SQRT分解に似たものがありますが、何らかの理由で合計をキャッシュせずに)、整数でスケーリングを適用する方法を見ることができます。



Source: https://habr.com/ru/post/J435426/


All Articles