Microsoft Excel如何与行高一起使用

有时我很无聊,并且配备了调试器,然后开始研究不同的程序。 这次,我的选择落在Excel上,希望弄清楚它如何处理行的高度,它存储的内容,如何考虑单元格范围的高度等。 我解析了Excel 2010(excel.exe,32位,版本14.0.4756.1000,SHA1 a805cf60a5542f21001b0ea5d142d1cd0ee00b28)。


让我们从理论开始


如果转到Microsoft Office的VBA文档,则可以看到行高度可以通过两个属性以一种或另一种方式获得:


  • RowHeight-返回或设置指定范围内的第一行的高度,以为单位。 读/写Double;
  • 高度 -返回一个Double值,该值表示范围的高度(以磅为单位)。 只读的。

如果您也在此处查看: Excel规范和限制 。 您可以找到最大行高为409点。 不幸的是,这远非唯一的Microsoft官方文件具有误导性的情况。 实际上,在Excel代码中,最大行高设置为2047像素,以点为1535.25。 且最大字体大小为409.55磅。 仅通过在VBA / Interop中分配行就不可能获得如此高的行,但是您可以一行,在其第一个单元格中设置Cambria Math字体,并将字体大小设置为409.55点。 然后,Excel及其狡猾算法将根据单元格格式计算行高,获取超过2047像素(相信单词)的数字,并将行设置为最大可能的高度。 如果通过UI询问该系列的高度,则Excel会认为该高度是409.5点,但是如果您通过VBA请求该系列的高度,则您会得到1535.25点,等于2047像素。 是的,保存文档后,高度仍将降至409.5点。 可以在以下视频中看到此操作: http : //recordit.co/ivnFEsELLI


我在上一段中提到像素是有原因的。 Excel实际上以整数形式存储和计算单元格的大小(通常,它会尽可能多地执行整数操作)。 最常见的是像素乘以某个因子。 有趣的是,Excel以普通分数的形式存储外观比例,例如,比例比例75%将被存储为两个数字3和4。当需要显示行时,Excel将行高作为整数像素,乘以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中的所有位的含义(要弄清楚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根据格式计算行高时,它表示为两个数字的总和。 defaultRowDelta2是此总和中第二个数字(标准行高度)。 flag参数的值也很神秘,因为 无论我在哪里看到在flag中传递给此方法的调用都传递false。
SheetLayoutInfo类也出现在此方法中。 我之所以这样命名,是因为它存储了大量有关工作表外观的信息。 在SheetLayoutInfo中,有如下字段:


  • DefaultFullRowHeightMul4-标准行高;
  • MinRowIndexNonDefault-第一行的索引,高度不同于标准行;
  • MaxRowIndexNonDefault-最后一个序列之后的序列的索引,该序列的高度不同于标准;
  • DefaultRowDelta2与标准行高的总和相同。
  • GroupIndexDelta-稍后会详细介绍

原则上,这种方法的逻辑是可以理解的:


  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以16件为一组存储RowHeightInfo,其中以RowsGroupInfo类表示的第i组将存储有关从i×16到i×16 + 15(含)的行的信息。


但是RowsGroupInfo中的行高信息以某种不寻常的方式存储。 最有可能是由于需要在Excel中维护历史记录。


RowsGroupInfo中有三个重要字段:Indices,HeightInfos和RowsCount,第二个在上面的代码中不可见(它应该在此行中:(rowsGroupInfo + 8×(...)),因为rowInfoIndex可以采用非常不同的值,我已经看过1000多个,而且我不知道如何在IDA中设置这样的结构。RowsCount字段没有出现在上面的代码中,但这是组中存储了多少个真正的非标准行。
另外,在SheetLayoutInfo中有GroupIndexDelta-组的实际索引与具有更改的行高的第一个组的索引之间的差。


索引字段存储组中系列的每个索引的RowHeightInfo偏移量。 它们按顺序存储在此处,但是在HeightInfos中,RowHeightInfo已按更改顺序存储。


假设我们有一个新的空白表,并且以某种方式更改了行号23的高度。此行位于第二组的16行中,然后:


  1. Excel将确定该系列的组索引。 在当前情况下,索引将为1,并将更改GroupIndexDelta = -1。
  2. 为一系列行创建RowsGroupInfo类的对象,并将其放在索引0(sheetLayoutInfo-> GroupIndexDelta + 1)下的sheetLayoutInfo-> RowsGroups中;
  3. 在RowsGroupInfo中,Excel将为16个4字节索引分配内存,分别用于RowsCount,wordBA,wordBC和wordB8等。
  4. 然后,Excel通过按位与运算(比采用除法的其余部分要快得多)来计算组中系列的索引:rowIndex&0xF。 该组中的期望索引为:23&0xF = 7;
  5. 之后,Excel获得索引7的偏移量:offset = Indices [7]。 如果offset = 0,则Excel在RowsGroupInto的末尾分配8个字节,将RowsCount增加1,然后将新的偏移量写入索引[7]。 无论如何,最后,Excel都会在RowsGroupInfo中的偏移处写入有关新行高和标志的信息。

C#本身中的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

在这里和下面的示例中,我将布局一个示意图,说明Excel内存中的数据是如何通过自写类在Visual Studio中生成的,因为从内存中直接转储的信息不多。
现在,让我们尝试隐藏第23行。为此,将Flags设置为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
第二组位移:第二组位移:
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
第二组中的行的高度值:第二组中的行的高度值:
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
第三组位移:第三组位移:
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
第三组中的行的高度值:第三组中的行的高度值:
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
第四组的偏移量:第四组的偏移量:
_00
第四组中的行的高度值:第四组中的行的高度值:
_40

这是预期的:Excel将第二行插入索引为7(39&0xF)的新行,其偏移量为0x05,将行高度复制到索引6,而最后一行(偏移量为05)被推到下一行组,然后从那里将最后一行推到第四行,依此类推。


现在让我们看看如果删除第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
第二组位移:第二组位移:
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
第二组中的行的高度值:第二组中的行的高度值:
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
第三组位移:第三组位移:
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
第三组中的行的高度值:第三组中的行的高度值:
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
第四组的偏移量:第四组的偏移量:
0000
第四组中的行的高度值:第四组中的行的高度值:
4050

原则上,当删除一行时,会发生反向操作。 在这种情况下,第四组继续存在,并且该行的高度值被标准高度填充,并带有相应的标志-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/zh-CN435426/


All Articles