Teknisk artikel

Temafärger för Excel-diagram i Delphi: HotXLS GelFrame

En diagramserie som fylls med en bokstavlig RGB-färg följer inte arbetsbokens tema. Ändra temat och serien behåller den gamla färgen. HotXLS hanterar detta i binär XLS med temafärgade fyllningar för diagramserier: en GelFrame-post, 4198 eller $1066, skriven direkt efter AreaFormat inne i serieblocket och med ett OfficeArt-schemindex plus en färgton. Excel renderar sedan serien på samma sätt som en temafyllning som det självt har skrivit

Varifrån kommer GelFrame-postnumret?

GelFrame-postnumret är 4198 ($1066), och du hittar det inte i postens eget specifikationsavsnitt. [MS-XLS] 2.4.131 beskriver vad en GelFrame innehåller, men till skillnad från de flesta postavsnitt anger det inte rt-värdet. Diagramunderströmmens ABNF hjälper inte heller: den ger bara produktionen GELFRAME = 1*2GelFrame *Continue, som namnger posten utan att numrera den. Numret finns i tabellen över postnummer, flera sidor från avsnittet som dokumenterar nyttolasten. Den produktionen är värd en extra titt för den som skriver en läsare: den tillåter en eller två GelFrame-poster, var och en valfritt följd av Continue-poster, så en parser som antar en enda post per produktion kommer att hantera en fil den inte skrev fel. HotXLS avger exakt en GelFrame per temafärgad serie, vilket är vad Excel producerar för en enkel solid temafyllning, och avkodaren behandlar posten som en självständig nyttolast i stället för att anta ett fast antal

Inne i GelFrame-nyttolasten: två OfficeArt-egenskapstabeller

GelFrame-nyttolasten är två OfficeArt-egenskapstabeller efter varandra: en OfficeArtFOPT (kallad OPT1) följd av en OfficeArtTertiaryFOPT (OPT2). Varje tabell är ett tvåbyte-antal egenskaper följt av så många sexbyte-poster av typen FOPTE, och varje post är ett tvåbyte-opid plus ett fyrabyte-op. Bit 15 i opid är fComplex: när den är satt är värdet op en bytelängd och en variabel svans följer efter de fasta posterna. En avkodare som ignorerar svansarna tappar synkroniseringen och läser skräpiga opid:n för allt efter den första komplexa egenskapen

Temafyllningen uttrycks av tre egenskaper spridda över båda tabellerna, plus en som deklarerar fyllningstypen. HotXLS skriver fyra egenskaper på 28 byte utan komplexa svansar:

  • fillType $0180 i OPT1, satt till 1 (msofillSolid)
  • fillColor $0181 i OPT1, den platta RGB-färg som en äldre eller temauvets konsument kommer att rita
  • fillColorExt $019E i OPT2, basfärgen från temat
  • fillColorExtMod $01A0 i OPT2, färgtonen eller skuggningen som tillämpas på basen

Den uppdelningen är avsiktlig i formatet, inte en implementationstillfällighet: [MS-ODRAW] 2.2.2 beskriver tematriaden som en platt färg plus en basfärg plus en modifiering, så en konsument som förstår teman räknar om fyllningen medan en som inte gör det ändå målar något rimligt. De omgivande opid:erna följer samma mönster och bär identisk numrering i gamla och aktuella utgåvor av [MS-ODRAW], vilket är praktiskt när du läser två revisioner parallellt: fillOpacity $0182, fillBackColor $0183, fillShadeType $019C, fillBackColorExt $01A2 och fillBackColorExtMod $01A4

Varför ligger schemindexet i den röda byten?

För att en OfficeArtCOLORREF definieras av byteoffset, inte av numeriskt värde: rött vid byte 0, grönt vid byte 1, blått vid byte 2 och flaggor vid byte 3. Läs strukturen som en little-endian-DWORD, vilket varje FOPTE-op är, så blir rött den minst signifikanta byten. Det genomarbetade exemplet med lineColor i [MS-ODRAW] bekräftar det. Så fSchemeIndex, som är flaggbiten E, har det numeriska värdet $08000000, och själva schemindexet går i den röda byten medan grönt och blått måste vara noll. Accent1 blir därför op-värdet $08000004, inte $00000004 och definitivt inte $04000000

Temats indexordning som specifikationen vägrar definiera

Specifikationen kallar ordningen för schemindex värddefinierad och ger ingen tabell, vilket betyder att byte-layouten ensam inte räcker för interoperabilitet med Excel. HotXLS använder kalkylbladets ordning för temat, vilket är den som gör tur och retur mot riktiga Excel-filer:

  • 0 = lt1, 1 = dk1, 2 = lt2, 3 = dk2
  • 4 till 9 = accent1 till accent6
  • 10 = hlink, 11 = folHlink

Färgton och skuggning: nyttolasten MSOTINTSHADE

fillColorExtMod-värdet är ett MSOTINTSHADE-värde och kodar riktning och mängd i ett DWORD i stället för som ett signerat bråk. Värdet $20000000 betyder oförändrat. En ljusare tint är $02F4 shl 16 or amount shl 8 or $10 (MSOTINT); en mörkare tint har samma form med $01F4 i det höga ordet (MSOSHADE). Byten amount går motsatt intuitiv riktning: $FF betyder oförändrat och $00 betyder full modifiering. HotXLS normaliserar det till ett enda DrawingML-liknande double där positivt ljusar upp och negativt mörkar, med plus eller minus (255 - amount) / 255. Avbildningen är exakt för de värden som Excel faktiskt erbjuder i sitt gränssnitt, vilket är varför tur och retur är förlustfri snarare än ungefärligt förlustfri: den välbekanta "Lighter 40%" är amount 153, och (255 - 153) / 255 är 0,4 utan avrundningsfel i någon riktning. En skuggning med amount 191 kommer tillbaka som -64/255. Här är kodaren, klampad till det lagliga intervallet:

if Tint > 0 then                       // MSOTINT - ljusare
  TintOp := LongWord($02F4) shl 16 or
    (LongWord(Round(255 * (1 - Tint))) shl 8) or $10
else if Tint < 0 then                  // MSOSHADE - mörkare
  TintOp := LongWord($01F4) shl 16 or
    (LongWord(Round(255 * (1 + Tint))) shl 8) or $10
else
  TintOp := $20000000;                 // MSOCOLORMODUNDEFINED

Ställ in och läs en temafyllning från Delphi

På skrivsidan är en temafyllning två extra fält på stilposten per serie. TXLSChartSeriesStyleInfo fick HasFillTheme, FillThemeColor och FillThemeTint, och byggaren avger GelFrame endast när både HasStyle och HasFillTheme är satta. Om du också sätter ett explicit FillRgb-värde går det in i OPT1:s fillColor ordagrant; om du inte gör det plattar HotXLS själv ut färgen genom en inbyggd standardtabell för Office-temat med färgtonen tillämpad, så en serie som bara har tema ändå har en rimlig platt färg för konsumenter som ignorerar OPT2. Lägg märke till Default()-initieringen, som spelar roll eftersom TXLSChartSeriesInfo innehåller hanterade fält och dess vanliga booleska medlemmar annars är skräp från stacken:

var
  Wb: TXLSWorkbook;
  Series: array [0..1] of TXLSChartSeriesInfo;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Sheets.Add.Name := 'Data';

    Series[0] := Default(TXLSChartSeriesInfo);   // använd aldrig FillChar för denna post
    Series[0].Name := 'Explicit';
    Series[0].Categories := 'Data!$A$1:$A$2';
    Series[0].Values := 'Data!$B$1:$B$2';
    Series[0].HasStyle := True;
    Series[0].Style.HasFill := True;
    Series[0].Style.FillRgb := $C47244;          // accent1, rött i lågbyte
    Series[0].Style.HasFillTheme := True;
    Series[0].Style.FillThemeColor := 4;         // accent1
    Series[0].Style.FillThemeTint := 0.4;        // Lighter 40%

    Series[1] := Default(TXLSChartSeriesInfo);
    Series[1].Name := 'ThemeOnly';
    Series[1].Categories := 'Data!$A$1:$A$2';
    Series[1].Values := 'Data!$C$1:$C$2';
    Series[1].HasStyle := True;
    Series[1].Style.HasFillTheme := True;        // ingen explicit RGB: plattas ut
    Series[1].Style.FillThemeColor := 8;         // accent5

    Wb.Sheets.AddChartSheet('Themed', xlsChartTypeColumn, '', '', '', Series);
    Wb.SaveAs('themed.xls');
  finally
    Wb.Free;
  end;
end;

Att läsa tillbaka går genom samma diagrammodell som resten av HotXLS-diagraminspektionen använder. GetChartModel returnerar en ägd TXLSChartModel som du frigör, och varje TXLSChartSeries exponerar HasFillTheme, FillThemeColor och FillThemeTint bredvid FillRgb som avkodats från OPT1:s fillColor, vilket har företräde framför AreaFormat-färgen för serien. Samma tre värden når också den kanoniska semantiska ögonblicksbilden som SolidFillThemeSet, SolidFillThemeColor och SolidFillThemeTint, så en arbetsboksskillnad ser en temaförändring som en temaförändring i stället för som en oförklarad RGB-drift. Om du kommer från XLSX-sidan är detta den binära formatmotsvarigheten till formgivningen som beskrivs i HotXLS-guiden till Excel-diagram, bilder och ritningar i Delphi:

Wb := TXLSWorkbook.Create;
try
  Wb.Open('themed.xls');
  Model := Wb.Sheets[2]._Chart.GetChartModel;
  try
    Ser := Model.GetSeries(0);
    if Ser.HasFillTheme then
    begin
      WriteLn(Ser.FillThemeColor);            // 4 = accent1
      WriteLn(Ser.FillThemeTint:0:3);         // 0.400
      WriteLn(IntToHex(Ser.FillRgb, 6));      // C47244, OPT1 fillColor
    end;
  finally
    Model.Free;
  end;
finally
  Wb.Free;
end;

Vad lovar en temafyllning i binär XLS inte?

Tre ärliga begränsningar. Först och viktigast för den som granskar denna kod: ingen exempelfil i den lokala korpusen innehåller alls en GelFrame-post. De elva förekomsterna av byteparet 66 10 i exemplet med villkorsformatering ligger vid gränser som inte är postgränser, och en fullständig postdump av strömmen hittar noll träffar. Bitlayouten som beskrivs här härleddes från specifikationen och låstes sedan på tre sätt, genom dekoderingssymmetri på byggarens utdata, genom handbyggda bytetester som matar en syntetisk $1066-nyttolast direkt till avkodaren och genom att hävda exakt platt RGB. Det är en svagare evidensform än en insamlad Excel-fil, och det är värt att säga i stället för att antyda något annat. För det andra använder utplattningen för en fyllning som bara har tema en inbyggd standardtabell för Office-temat, inte en temadel som läses från arbetsboken, eftersom binär XLS inte har någon temadel i den mening som en paketerad XLSX har — om du behöver arbetsbokens eget tema för att styra den platta färgen ska du själv ange FillRgb. För det tredje accepterar avkodaren endast en GelFrame inne i ett serieblock; samma post kan förekomma på diagramområdet eller en axelram, och att acceptera den där skulle tyst tillskriva en bakgrundsfyllning till en serie, så sådana ignoreras. En fillColorExt utan flaggan $08000000 behandlas på samma sätt som en vanlig utökad färg och sätter aldrig HasFillTheme. För arbetsböcker där diagrammet har skapats i XLSX-världen och bara passerar genom systemet är bevarandepathen i att redigera Excel-diagram utan att förlora ChartML den säkrare vägen, och behållaren som posterna ligger i täcks i att läsa OLE2-sammansättningsfiler i Delphi utan COM IStorage

Temafärgade diagramfyllningar, GelFrame-kodaren och avkodaren samt hela BIFF8-diagramunderströmsbyggaren levereras i HotXLS Delphi-kalkylbladskomponenten för Delphi och C++Builder, som läser och skriver XLS, XLSX och ODS utan Excel installerat