- PVSM.RU - https://www.pvsm.ru -

Как Microsoft Excel работает с высотами рядов

Иногда мне бывает скучно и я, вооружившись отладчиком, начинаю копаться в разных программах. В этот раз мой выбор пал на Excel и было желание разобраться как он оперирует высотами рядов, в чём хранит, как считает высоту диапазона ячеек и т.д. Разбирал я Excel 2010 (excel.exe, 32bit, version 14.0.4756.1000, SHA1 a805cf60a5542f21001b0ea5d142d1cd0ee00b28).

Начнём с теории

Если обратиться к документации по VBA для Microsoft Office, то можно увидеть, что высоту ряда так или иначе можно получить через два свойства:

  • RowHeight [1] — Returns or sets the height of the first row in the range specified, measured in points. Read/write Double;
  • Height [2] — Returns a Double value that represents the height, in points, of the range. Read-only.

Причём если заглянуть ещё и сюда: Excel specifications and limits [3]. То можно обнаружить, что максимальная высота ряда составляет 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 [4]

Я не зря упомянул в предыдущем абзаце пиксели. 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, то в Value будет хранится 400. Что обозначают все биты в 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. В противном случае получаем объект rowHeightInfo для ряда из метода GetRowHeightCore;
  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 штук, где i-я группа, представленная классом RowsGroupInfo, будет хранить в себе информацию о рядах с i × 16 до i × 16 + 15 включительно.

Но информация о высоте рядов в RowsGroupInfo хранится несколько необычным способом. Скорее всего из-за необходимости поддерживать историю в Excel.

В RowsGroupInfo есть три важных поля: Indices, HeightInfos, и RowsCount, второе в коде выше не видно (оно должно быть в этой строчке: (rowsGroupInfo + 8 × (...)), т.к. rowInfoIndex может принимать очень разные значения, я видел даже больше 1000 и я понятия не имею как в IDA задать такую структуру. Поле RowsCount вообще в коде выше не встречается, но именно там хранится сколько реально нестандартных рядов хранится в группе.
Кроме того, в SheetLayoutInfo есть GroupIndexDelta — разница между реальным индексом группы и индексом первой группы с изменённой высотой ряда.

В поле Indices хранятся смещения RowHeightInfo для каждого индекса ряда внутри группы. Они хранятся там по порядку, а вот в HeightInfos RowHeightInfo уже хранятся в порядке изменения.

Допустим у нас есть новый пустой лист и мы каким-то образом изменили высоту ряда номер 23. Это ряд лежит во второй группе из 16 рядов, тогда:

  1. Excel определит индекс группы для этого ряда. В текущем случае индекс будет равен 1 и изменит GroupIndexDelta = -1.
  2. Создаст для группы рядов объект класса RowsGroupInfo и положит его в sheetLayoutInfo->RowsGroups под индексом 0 (sheetLayoutInfo->GroupIndexDelta + 1);
  3. В RowsGroupInfo Excel выделит память под 16 4-х байтовых Indices, под RowsCount, wordBA, wordBC и wordB8 и т.д..;
  4. Потом Excel вычисляет индекс ряда в группе через операцию побитового И (это сильно быстрее чем брать остаток от деления): rowIndex & 0xF. Искомый индекс в группе будет равняться: 23 & 0xF = 7;
  5. После этого Excel получает смещение для индекса 7: offset = Indices[7]. Если offset = 0, то Excel выделяет 8 байт в конце RowsGroupInto, увеличивает RowsCount на единицу и записывает новое смещение в Indices[7]. В любом случае в конце Excel запишет по смещению в RowsGroupInfo информацию о новой высоте ряда и флагах.

Сам класс RowsGroupInfo на C# выглядел бы вот так:

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 Count = 1
      • [0] RowHeightInfo
      • Flags = 0x4005
      • Value = 100
    • Indices
      • [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. Для этого надо выставить бит 0x2000 у Flags. Будем изменять память на живую. Результат можно увидеть на этом видео: http://recordit.co/79vYIbwbzB [5].
При любом скрытии рядов Excel поступает также.
Теперь зададим высоту ряда не явно, а через формат ячейки. Пускай у ячейки A20 шрифт станет высотой 40 пунктов, тогда высота ячейки в точках станет 45,75 и в памяти Excel будет примерно такое:

sheetLayoutInfo

  • DefaultFullRowHeightMul4 = 80
  • DefaultRowDelta2 = 5
  • MaxRowIndexNonDefault = 24
  • MinRowIndexNonDefault = 20
  • GroupIndexDelta = -1
  • RowsGroups Count = 1
    • [0] RowsGroupInfo
    • HeightInfos Count = 2
      • [0] RowHeightInfo
      • Flags = 0x4005
      • Value = 100
      • [1] RowHeightInfo
      • Flags = 0x800E
      • Value = 244
    • Indices
      • [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, 02 0E, 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, 100 05, 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, 05 06, 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, F0 10, 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, 03 03, 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, FB 0B, 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, 02 0E, 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, 100 05, 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, 0D 02, 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, F0 10, 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, 04 0C, 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, FB 0B, 1B, 3B, 40, 55, 60, 65, 70, 80, 95, BB, C0, DB, EB, F5, FB
Смещения в четвёртой группе: Смещения в четвёртой группе:
00 00
Значения высот рядов в четвёртой группе: Значения высот рядов в четвёртой группе:
40 50

В принципе при удалении ряда происходят операции обратные вставке. При этом четвёртая группа продолжает существовать и значение высоты ряда там заполняется стандартной высотой с соответствующим флагом — 0x8005.

Этих данных достаточно для того, чтобы воспроизвести этот алгоритм на C#:

InsertRow

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);
    }
}

RemoveRow

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 [6]

Выводы

Код Excel уже не в первый раз удивляет интересными приёмами. В этот раз я узнал как он хранит информацию о высотах рядов. Если сообществу будет интересно, то в следующей статье покажу как Excel считает высоту диапазона ячеек (спойлер: там что-то похожее на SQRT-декомпозицию, но почему-то без кеширования сумм), там же можно будет увидеть как он применяет масштабирование в целых числах.

Автор: Юдаков Дмитрий

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/305027

Ссылки в тексте:

[1] RowHeight: https://docs.microsoft.com/en-us/office/vba/api/excel.range.rowheight

[2] Height: https://docs.microsoft.com/en-us/office/vba/api/excel.range.height

[3] Excel specifications and limits: https://support.office.com/en-us/article/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3

[4] http://recordit.co/ivnFEsELLI: http://recordit.co/ivnFEsELLI

[5] http://recordit.co/79vYIbwbzB: http://recordit.co/79vYIbwbzB

[6] Весь вышеописанный код вы можете найти на GitHub: https://gist.github.com/tdkkdt/1cc84ac8a9e201201f8b5aa2e5fe1ed2

[7] Источник: https://habr.com/post/435426/?utm_source=habrahabr&utm_medium=rss&utm_campaign=435426