Свежая версия статьи находится здесь: 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
Комментариев нет:
Отправить комментарий