Свежая версия статьи находится здесь: https://habr.com/ru/post/435426/
Иногда мне бывает скучно и я, вооружившись отладчиком, начинаю копаться в разных программах. В этот раз мой выбор пал на Excel и было желание разобраться как он оперирует высотами рядов, в чём хранит, как считает высоту диапазона ячеек и т.д. Разбирал я Excel 2010 (excel.exe, 32bit, version 14.0.4756.1000, SHA1 a805cf60a5542f21001b0ea5d142d1cd0ee00b28).
Я не зря упомянул в предыдущем абзаце пиксели. Excel на самом деле хранит и рассчитывает размеры ячеек в целых числах (он вообще максимально всё делает в целых числах). Чаще всего это пиксели, умноженные на некоторый коэффициент. Интересно, что Excel хранит масштаб внешнего вида в виде обыкновенной дроби, например, масштаб 75% будет храниться как два числа 3 и 4. И когда надо будет вывести на экран ряд, Excel возьмёт высоту ряда как целое число пикселей, умножит на 3 и разделит на 4. Но выполнять эту операцию он будет уже в самом конце от этого создаётся эффект, что всё считается в дробных числах. Чтобы в этом убедиться напишите в VBA вот такой код:
В принципе мы уже подобрались к тому, чтобы написать класс на C#, который будет описывать информацию о высоте ряда:
Что такое defaultRowDelta2 для меня тоже до сих пор остаётся загадкой. Когда excel рассчитывает высоту ряда на основе формата,то представляет её как сумму двух чисел. defaultRowDelta2 - это второе число из этой суммы для стандартной высоты ряда. Значение параметра flag тоже загадочно, т.к. везде где я видел вызов этого метода в flag передавался false.
В этом методе также появляется класс SheetLayoutInfo. Я его назвал именно так, потому что в нём хранится много всякой информации о внешнем виде листа. В SheetLayoutInfo есть такие поля как:
Но информация о высоте рядов в RowsGroupInfo хранится несколько необычным способом. Скорее всего из-за необходимости поддерживать историю в Excel.
В RowsGroupInfo есть три важных поля: Indices, HeightInfos, и RowsCount, второе в коде выше не видно (оно должно быть в этой строчке: (rowsGroupInfo + 8 × (…)) , т.к. rowInfoIndex может принимать очень разные значения, я видел даже больше 1000 и я понятия не имею как в IDA задать такую структуру. Поле RowsCount вообще в коде выше не встречается, но именно там хранится сколько реально нестандартных рядов хранится в группе.
Кроме того, в SheetLayoutInfo есть GroupIndexDelta - разница между реальным индексом группы и индексом первой группы с изменённой высотой ряда.
В поле Indices хранятся смещения RowHeightInfo для каждого индекса ряда внутри группы. Они хранятся там по порядку, а вот в HeightInfos RowHeightInfo уже хранятся в порядке изменения.
Допустим у нас есть новый пустой лист и мы каким-то образом изменили высоту ряда номер 23. Это ряд лежит во второй группе из 16 рядов, тогда:
При любом скрытии рядов Excel поступает также.
Теперь зададим высоту ряда не явно, а через формат ячейки. Пускай у ячейки A20 шрифт станет высотой 40 пунктов, тогда высота ячейки в точках станет 45,75 и в памяти Excel будет примерно такое:
Флаг a5 определяет какая именно сейчас происходит операция.
Получается то, что и ожидалось: Excel вставляет во вторую группу новый ряд с индексом 7 (39 & 0xF), у которого смещение равняется 0x05, копирует высоту ряда у индекса 6, при этом последний ряд, который был со смещением 05, выталкивается в следующую группу, а оттуда последний ряд выталкивается в четвёртую и т.д.
Теперь посмотрим, что происходит если удалить 29-й ряд.
В принципе при удалении ряда происходят операции обратные вставке. При этом четвёртая группа продолжает существовать и значение высоты ряда там заполняется стандартной высотой с соответствующим флагом - 0x8005.
Этих данных достаточно для того, чтобы воспроизвести этот алгоритм на C#:
Иногда мне бывает скучно и я, вооружившись отладчиком, начинаю копаться в разных программах. В этот раз мой выбор пал на Excel и было желание разобраться как он оперирует высотами рядов, в чём хранит, как считает высоту диапазона ячеек и т.д. Разбирал я Excel 2010 (excel.exe, 32bit, version 14.0.4756.1000, SHA1 a805cf60a5542f21001b0ea5d142d1cd0ee00b28).
Начнём с теории
Если обратиться к документации по 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.
Я не зря упомянул в предыдущем абзаце пиксели. Excel на самом деле хранит и рассчитывает размеры ячеек в целых числах (он вообще максимально всё делает в целых числах). Чаще всего это пиксели, умноженные на некоторый коэффициент. Интересно, что Excel хранит масштаб внешнего вида в виде обыкновенной дроби, например, масштаб 75% будет храниться как два числа 3 и 4. И когда надо будет вывести на экран ряд, Excel возьмёт высоту ряда как целое число пикселей, умножит на 3 и разделит на 4. Но выполнять эту операцию он будет уже в самом конце от этого создаётся эффект, что всё считается в дробных числах. Чтобы в этом убедиться напишите в VBA вот такой код:
w.Rows(1).RowHeight = 75.375 Debug.Print w.Rows(1).HeightVBA выдаст 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 - об этом позже
- Если индекс ряда меньше первого с нестандартной высотой, то возвращаем стандартную;
- Если индекс ряда больше последнего с нестандартной высотой, то возвращаем стандартную;
- В противном случае получаем объект rowHeightInfo для ряда из метода GetRowHeightCore;
- Если rowHeightInfo == null возвращаем стандартную высоту ряда;
- Тут магия с флагами, но в общем виде мы возвращаем то, что находится в rowHeightInfo.Value и выставляем 16-й бит в ответе, если высота ряда не была задана вручную.
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; }
- Опять в начале Excel проверяет находится ли индекс ряда среди рядов с изменённой высотой и если нет, то возвращает null.
- Находит нужную группу рядов, если такой группы нет, то возвращает null.
- Получает индекс ряда в группе.
- Далее по индексу ряда находит нужный объект класса RowHeightInfo. wordBA, wordBC, wordB8 - какие-то константы. Они изменяются только вместе с историей. В принципе на понимание алгоритма они не влияют.
Но информация о высоте рядов в RowsGroupInfo хранится несколько необычным способом. Скорее всего из-за необходимости поддерживать историю в Excel.
В RowsGroupInfo есть три важных поля: Indices, HeightInfos, и RowsCount, второе в коде выше не видно (оно должно быть в этой строчке: (rowsGroupInfo + 8 × (…)) , т.к. rowInfoIndex может принимать очень разные значения, я видел даже больше 1000 и я понятия не имею как в IDA задать такую структуру. Поле RowsCount вообще в коде выше не встречается, но именно там хранится сколько реально нестандартных рядов хранится в группе.
Кроме того, в SheetLayoutInfo есть GroupIndexDelta - разница между реальным индексом группы и индексом первой группы с изменённой высотой ряда.
В поле Indices хранятся смещения RowHeightInfo для каждого индекса ряда внутри группы. Они хранятся там по порядку, а вот в HeightInfos RowHeightInfo уже хранятся в порядке изменения.
Допустим у нас есть новый пустой лист и мы каким-то образом изменили высоту ряда номер 23. Это ряд лежит во второй группе из 16 рядов, тогда:
- Excel определит индекс группы для этого ряда. В текущем случае индекс будет равен 1 и изменит GroupIndexDelta = -1.
- Создаст для группы рядов объект класса RowsGroupInfo и положит его в sheetLayoutInfo->RowsGroups под индексом 0 (sheetLayoutInfo->GroupIndexDelta + 1);
- В RowsGroupInfo Excel выделит память под 16 4-х байтовых Indices, под RowsCount, wordBA, wordBC и wordB8 и т.д…;
- Потом Excel вычисляет индекс ряда в группе через операцию побитового И (это сильно быстрее чем брать остаток от деления): rowIndex & 0xF. Искомый индекс в группе будет равняться: 23 & 0xF = 7;
- После этого Excel получает смещение для индекса 7: offset = Indices[7]. Если offset = 0, то Excel выделяет 8 байт в конце RowsGroupInto, увеличивает RowsCount на единицу и записывает новое смещение в Indices[7]. В любом случае в конце Excel запишет по смещению в 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 точек):- DefaultFullRowHeightMul4 = 80
- DefaultRowDelta2 = 5
- MaxRowIndexNonDefault = 24
- MinRowIndexNonDefault = 23
- GroupIndexDelta = -1
- RowsGroups Count = 1
- [0] RowsGroupInfo
- HeightInfos Count = 1
- [0] RowHeightInfo
- Flags = 0x4005
- Value = 100
- [0] RowHeightInfo
- 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 из самописных классов, потому что прямой дамп из памяти не сильно информативен.
- HeightInfos Count = 1
- [0] RowsGroupInfo
При любом скрытии рядов Excel поступает также.
Теперь зададим высоту ряда не явно, а через формат ячейки. Пускай у ячейки A20 шрифт станет высотой 40 пунктов, тогда высота ячейки в точках станет 45,75 и в памяти Excel будет примерно такое:
- 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
- [0] RowHeightInfo
- 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 всё равно посчитает её один раз и занесёт результат в соответствующую группу.
- HeightInfos Count = 2
- [0] RowsGroupInfo
Разбираемся со вставкой/удалением рядов
Интересно было бы разобрать что происходит при вставке/удалении рядов. Соответствующий код в excel.exe найти несложно, но разбирать его не было желания, можете сами взглянуть на часть из него:Флаг 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 |
Теперь посмотрим, что происходит если удалить 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 |
Этих данных достаточно для того, чтобы воспроизвести этот алгоритм на 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
Комментариев нет:
Отправить комментарий