Export HTML Tables to PowerPoint with WebView2 and VSTO

export-html-tables-to-powerpoint-with-webview2-and-vsto

In this blog post, we’ll walk through building a complete solution that allows users to hover over HTML tables in a WebView2-based application, click an export button, and send the table data—complete with styling and merge info—to a VSTO PowerPoint Add-in that renders a native PowerPoint table.

Overview

Components:

  1. WebView2 Application (WinForms or WPF)
  2. JavaScript Injection to extract table data and styles
  3. C# VSTO Add-in to receive and render the PowerPoint table

Step 1: Setup WebView2

Install the Microsoft.Web.WebView2 NuGet package. Then initialize and navigate:

await webView2.EnsureCoreWebView2Async(null);
webView2.CoreWebView2.Navigate("https://your-webpage.com");

Step 2: Inject JavaScript to Export Tables

Inject the following JavaScript to add an export button and capture table data:

(function () {
    const style = document.createElement("style");
    style.textContent = `
        .export-btn {
            position: absolute;
            padding: 4px 8px;
            font-size: 12px;
            background-color: #0078D4;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            display: none;
            z-index: 10000;
        }
    `;
    document.head.appendChild(style);

    const exportBtn = document.createElement("button");
    exportBtn.className = "export-btn";
    exportBtn.textContent = "Export Table";
    document.body.appendChild(exportBtn);

    let currentTable = null;

    document.addEventListener("mouseover", (e) => {
        const table = e.target.closest("table");
        if (table && table !== currentTable) {
            currentTable = table;
            const rect = table.getBoundingClientRect();
            exportBtn.style.top = `${window.scrollY + rect.top - 30}px`;
            exportBtn.style.left = `${window.scrollX + rect.left}px`;
            exportBtn.style.display = "block";
        }
    });

    document.addEventListener("mouseout", (e) => {
        if (!e.relatedTarget || !e.relatedTarget.closest("table")) {
            exportBtn.style.display = "none";
            currentTable = null;
        }
    });

    exportBtn.addEventListener("click", () => {
        if (!currentTable) return;

        const getStyles = (el) => {
            const style = window.getComputedStyle(el);
            return {
                fontSize: style.fontSize,
                fontFamily: style.fontFamily,
                color: style.color,
                backgroundColor: style.backgroundColor,
                fontWeight: style.fontWeight
            };
        };

        const data = {
            headers: [],
            rows: [],
            rowHeights: [],
            theme: getStyles(currentTable),
            merges: []
        };

        const rows = currentTable.querySelectorAll("tr");
        rows.forEach((row, rIndex) => {
            const cells = row.querySelectorAll("th, td");
            const rowData = [];
            let colPos = 0;

            cells.forEach((cell) => {
                const colspan = parseInt(cell.getAttribute("colspan") || "1");
                const rowspan = parseInt(cell.getAttribute("rowspan") || "1");

                if (colspan > 1 || rowspan > 1) {
                    data.merges.push({ row: rIndex, col: colPos, colspan, rowspan });
                }

                rowData.push({ text: cell.innerText.trim(), style: getStyles(cell) });
                colPos += colspan;
            });

            if (row.querySelector("th")) data.headers.push(rowData);
            else data.rows.push(rowData);

            data.rowHeights.push(row.offsetHeight);
        });

        window.chrome.webview.postMessage({ type: "tableExport", payload: data });
    });
})();

Step 3: Handle WebView2 Messages in C

webView2.CoreWebView2.WebMessageReceived += WebView2_WebMessageReceived;

private void WebView2_WebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e)
{
    string json = e.WebMessageAsJson;
    var obj = JsonConvert.DeserializeObject<JObject>(json);
    if (obj["type"]?.ToString() == "tableExport")
    {
        var payload = obj["payload"]?.ToString();
        var data = JsonConvert.DeserializeObject<TableExportData>(payload);
        GeneratePowerPointTable(data);
    }
}

Step 4: Define Data Models

public class TableCellData
{
    public string text { get; set; }
    public Style style { get; set; }
}

public class Style
{
    public string fontSize { get; set; }
    public string fontFamily { get; set; }
    public string color { get; set; }
    public string backgroundColor { get; set; }
    public string fontWeight { get; set; }
}

public class MergeInfo
{
    public int row { get; set; }
    public int col { get; set; }
    public int colspan { get; set; }
    public int rowspan { get; set; }
}

public class TableExportData
{
    public List<List<TableCellData>> headers { get; set; }
    public List<List<TableCellData>> rows { get; set; }
    public List<int> rowHeights { get; set; }
    public Style theme { get; set; }
    public List<MergeInfo> merges { get; set; }
}

Step 5: Generate PowerPoint Table

private void GeneratePowerPointTable(TableExportData data)
{
    var app = Globals.ThisAddIn.Application;
    var slide = app.ActivePresentation.Slides[1];

    int numRows = data.headers.Count + data.rows.Count;
    int numCols = data.headers.FirstOrDefault()?.Count ?? data.rows.FirstOrDefault()?.Count;

    var shape = slide.Shapes.AddTable(numRows, numCols, 50, 50, 600, 300);
    var table = shape.Table;

    int rowIndex = 1;
    foreach (var row in data.headers.Concat(data.rows))
    {
        int colIndex = 1;
        foreach (var cell in row)
        {
            var pptCell = table.Cell(rowIndex, colIndex);
            pptCell.Shape.TextFrame.TextRange.Text = cell.text;

            var style = cell.style;
            if (style != null)
            {
                pptCell.Shape.TextFrame.TextRange.Font.Name = style.fontFamily;
                pptCell.Shape.TextFrame.TextRange.Font.Size = ConvertFontSize(style.fontSize);
                pptCell.Shape.Fill.ForeColor.RGB = ConvertColor(style.backgroundColor);
                pptCell.Shape.TextFrame.TextRange.Font.Bold = style.fontWeight == "bold" ? MsoTriState.msoTrue : MsoTriState.msoFalse;
                pptCell.Shape.TextFrame.TextRange.Font.Color.RGB = ConvertColor(style.color);
            }
            colIndex++;
        }
        rowIndex++;
    }

    foreach (var merge in data.merges)
    {
        table.Cell(merge.row + 1, merge.col + 1).Merge(
            table.Cell(merge.row + merge.rowspan, merge.col + merge.colspan));
    }
}

Helper Methods

private int ConvertColor(string cssColor)
{
    var color = System.Drawing.ColorTranslator.FromHtml(cssColor);
    return color.B << 16 | color.G << 8 | color.R;
}

private float ConvertFontSize(string fontSize)
{
    return float.TryParse(fontSize.Replace("px", ""), out float px) ? px * 0.75f : 12f;
}

JSON Structure Sample

{
  "type": "tableExport",
  "payload": {
    "headers": [[{ "text": "Year", "style": {"fontSize": "14px", "fontFamily": "Arial", "color": "#fff", "backgroundColor": "#0078D4", "fontWeight": "bold"} }]],
    "rows": [[{ "text": "2023", "style": {"fontSize": "13px", "fontFamily": "Segoe UI"} }]],
    "rowHeights": [30, 28],
    "theme": { "fontSize": "14px", "fontFamily": "Arial" },
    "merges": [{ "row": 0, "col": 0, "colspan": 2, "rowspan": 1 }]
  }
}

Wrapping Up

This integrated solution demonstrates how to:

  • Extend WebView2 with JavaScript to capture styled table data.
  • Use messaging to transfer JSON from WebView2 to C#.
  • Dynamically build a styled, merged PowerPoint table with VSTO.
Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
how-to-setup-and-configure-nginx-on-ubuntu

How to setup and configure NGINX on Ubuntu

Next Post
real-gem

Real Gem

Related Posts