Хабрахабр

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

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

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

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

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

То можно обнаружить, что максимальная высота ряда составляет 409 точек. Причём если заглянуть ещё и сюда: Excel specifications and limits. На самом деле в коде Excel максимальная высота ряда задана как 2047 пикселя, что в точках будет 1535. Это к сожалению далеко не единственный случай, когда официальные документы Microsoft немножко лукавят. А максимальный размер шрифта 409. 25. Получить ряд такой огромной высоты простым присваиванием в VBA/Interop не получится, но можно взять ряд, задать его первой ячейке шрифт Cambria Math, а размер шрифта выставить в 409. 55 пунктов. Тогда Excel своим хитрым алгоритмом посчитает высоту ряда на основе формата ячейки, получит число превышающее 2047 пикселей (поверьте на слово) и сам выставит у ряда максимально возможную высоту. 55 пунктов. 5 точек, но если запросить высоту ряда через VBA, то получится честные 1535,25 точек, что равняется 2047 пикселям. Если спросить высоту этого ряда через UI, то Excel соврёт что высота 409. Эту манипуляцию можно пронаблюдать вот на этом видео: http://recordit.co/ivnFEsELLI Правда после сохранения документа высота всё равно сбросится до 409,5 точек.

Excel на самом деле хранит и рассчитывает размеры ячеек в целых числах (он вообще максимально всё делает в целых числах). Я не зря упомянул в предыдущем абзаце пиксели. Интересно, что Excel хранит масштаб внешнего вида в виде обыкновенной дроби, например, масштаб 75% будет храниться как два числа 3 и 4. Чаще всего это пиксели, умноженные на некоторый коэффициент. Но выполнять эту операцию он будет уже в самом конце от этого создаётся эффект, что всё считается в дробных числах. И когда надо будет вывести на экран ряд, Excel возьмёт высоту ряда как целое число пикселей, умножит на 3 и разделит на 4. Чтобы в этом убедиться напишите в VBA вот такой код:

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

75,375 точек будет 100,5 пикселей, а Excel такое себе позволить не может и отбросит дробную часть до 100 пикселей. VBA выдаст 75, т.к. Когда VBA будет запрашивать высоту ряда в точках, Excel честно переведёт 100 пикселей в точки и вернёт 75.

В принципе мы уже подобрались к тому, чтобы написать класс на C#, который будет описывать информацию о высоте ряда:

class RowHeightInfo //высота ряда в целых пикселях, умноженная на 4. public ushort Flags { get; set; } //дополнительные флаги
}

Т.е., если задано, что высота ряда 75 точек, в пикселях это будет 100, то в Value будет хранится 400. Вам пока что придётся поверить мне на слово, но в Excel высота ряда хранится именно так. В целом у видимых рядов с заданной вручную высотой Flags чаще всего равняется 0x4005, а для рядов у которых высота высчитывается на основе форматирования Flags равняется либо 0xA, либо 0x800E. Что обозначают все биты в Flags я до конца не выяснил (выяснять значения флагов сложно и долго), но знаю точно, что 0x4000 выставляется для рядов у которых высота задана вручную, а 0x2000 — выставляется у скрытых рядов.

Спрашиваем высоту ряда

Теперь в принципе можно взглянуть на метод из 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;
}

не смог найти место где этот флаг выставляется 🙁
Что такое defaultRowDelta2 для меня тоже до сих пор остаётся загадкой. Что такое dword1A0 я так и не выяснил, т.к. defaultRowDelta2 — это второе число из этой суммы для стандартной высоты ряда. Когда excel рассчитывает высоту ряда на основе формата, то представляет её как сумму двух чисел. везде где я видел вызов этого метода в flag передавался false.
В этом методе также появляется класс SheetLayoutInfo. Значение параметра flag тоже загадочно, т.к. В 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 — какие-то константы. Они изменяются только вместе с историей. В принципе на понимание алгоритма они не влияют.

Excel хранит RowHeightInfo группами по 16 штук, где i-я группа, представленная классом RowsGroupInfo, будет хранить в себе информацию о рядах с i × 16 до i × 16 + 15 включительно. Тут стоит отклониться от темы и рассказать подробнее про RowsGroupInfo.

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

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

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

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

  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.
При любом скрытии рядов 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 включительно в случайном порядке. К тому же по внешнему виду можно примерно понять что там происходит, а остальное добить по косвенным признакам.
Попытаемся обозначить эти косвенные признаки. Новый ряд будет копировать высоту у ряда 38.
Посмотрим на информацию в группах рядов до и после добавления ряда, я выделил жирным различия: Потом перед рядом под индексом 39 вставим новый ряд.

До добавления ряда

После добавления ряда

Смещения в первой группе:

Смещения в первой группе:

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

Выводы

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть