Where to Discuss?

Local Group

Preface

Goal: Separating formatting code and configuration. Example class that uses the unified configuration.

After covering the basic macro, we can now step into utilizing the unified configuration, and explore how far the formatting can go. We will transition all formatting procedures, from the simple configuration to the unified one.

Example Sheet

You can download the example ODS source file here:

And also example XLSX result here:

This is basically a workbook with 40 worksheets.


04: Additional Helper

A few helpful methods

A lengthy article section can be overwhelming. Let’s move all supporting helper methods here, before we dive into the class itself.

Calc Macro: Step Four: Helper: All Supporting Methods

There are some brand-new helper methods, that were not available in the previous example. I’ll list them all here so you can easily spot the differences:

  • _column_letter_to_index().
  • _get_relative_column_letter().
  • __get_number_format().

Helper: Column Letter To Index

This helper method converts Excel-style column letters to a 0-based column index. It will be used in various parts of the code later.

Calc Macro: Step Four: Helper: Letter to Index

Think of it as the translator between Python’s love for zero, and Excel’s obsession with letters.

  def _column_letter_to_index(self,
        column_letter: str) -> int:

    index = 0
    for i, char in enumerate(reversed(column_letter)):
      index += (ord(char) - ord('A') + 1) * (26 ** i)
    return index - 1

It’s the reverse of the previously mention method.

Calc Macro: Step Four: Helper: Index to Letter

Helper: Get Relative Column Letter

This helper method retrieves the Excel-style column letter at an offset from the start_letter. It will be used in several parts of the code.

Calc Macro: Step Four: Helper: Relative Column

  def _get_relative_column_letter(self,
        start_letter: str, offset: int) -> str:

    start_index    = self._column_letter_to_index(start_letter)
    relative_index = start_index + offset - 1
    return self._column_index_to_letter(relative_index)

Sheet Helper: Get Number Format

This helper method is designed to be used exclusively, within the _set_columns_format() function.

Calc Macro: Step Four: Helper: Get Number Format

  def __get_number_format(self,
        format_string: str) -> XNumberFormats:

    nf = self._numberfmt.queryKey(  \
              format_string, self._locale, True)
    if nf == -1:
       nf = self._numberfmt.addNew( \
              format_string, self._locale)
    return nf

Optionally, we can use type annotations with XNumberFormats.

Import Statements

We need to include additional import statements from com.sun.star, for the interface type.

Calc Macro: Step Four: Import Statements

from abc import ABC, abstractmethod

from com.sun.star.sheet import XSpreadsheetDocument
from com.sun.star.util  import XNumberFormats

from com.sun.star.\
  table.CellHoriJustify import LEFT, CENTER, RIGHT

Now we can use this helper in the class.


04: Step Four

TIn this section, we introduce configuration metadata, into the formatting process for the first time.

Step Overview

This section covers the following topics for unified configuration:

  • Class: BorderFormatManager
  • Formatting Procedures:
    • Add merged titles (for multiple column blocks)
    • Set merged title (refactored for individual blocks)
    • Add merged titles post (hook method)

Other topics follow the simple configuration pattern:

  • Configuration: Field Metadata Configuration
  • Helper Methods:
    • Letter to Index
    • Number Formats
  • Formatting Procedure: Format Columns

Macro Source

The macro source for this step can be found in the following repository:

Class Diagram

What’s new?

In addition to the three helper methods listed above, this step introduces:

  • Two abstract methods, each with an implementation
  • A supporting method
  • Three instance variables

Class Diagram: Formatting: Unified Configuration: Step 04

Abstract Method

This step adds two new abstract methods, one for initializing configuration metadata, and another for the column formatting procedure.

Calc Macro: Step Four: Abstract Method

  @abstractmethod
  def _init_metadatas(self) -> None:
    pass

  ...

  @abstractmethod
  def _set_columns_format(self) -> None:
    pass

Basic Flow

The _set_columns_format() method fits naturally, into the flow at this point.

Calc Macro: Step Four: Basic Flow

  def __format_one_sheet(self) -> None:
    ...

    # Apply Sheet Wide
    print(' * Formatting Columns')
    self._set_sheetwide_view()
    self._set_columns_format()

Initialization: Init Metadatas

The _init_metadatas() method is introduced here, alongside the initial value for the _metadatas variable.

Calc Macro: Step Four: Initialization

  def __init__(self) -> None:
    self._sheet = None
    self._controller = self._document.getCurrentController()
    self._gaps = []
    self._metadatas = []

    self.__prepare_sheet()
    self._init_metadatas()
    self._merge_metadatas()

We also have this __prepare_sheet() method.

Initialization: Prepare Sheet

A small but powerful setup.

The __prepare_sheet() method handles the initialization, of key instance variables for number and date formats. Streamlining the process right from the start.

Calc Macro: Step Four: Prepare Sheet

  def __prepare_sheet(self) -> None:
    self._numberfmt = self._document.NumberFormats
    self._locale    = self._document.CharLocale

I often use this trick in other macros as well. It keeps things tidy and consistent.

Configuration: Init Metadatas

Managing Instance Variables

To unify the configuration metadata, we start by defining fields as dictionaries. These dictionaries hold the column letter, width, and optional alignment properties.

The _init_metadatas() method organizes collections, for each block of columns. In this movie example, we work with two blocks:

  • _metadata_movies_base, and
  • _metadata_movies_additional.

Calc Macro: Step Four: Init Metadatas

The key difference from the simple configuration is that, column letters are no longer hardcoded individually. Instead, we determine column positions dynamically, by merging all blocks.

This process relies on a helper method called _get_relative_column_letter(), to find starting positions. Nomore manual adjustments every time the source sheet changes.

Here’s the setup:

  def _init_metadatas(self) -> None:
    self._metadata_movies_base = {
      'fields': {
        'Year'     : { 'width': 1.5, 'align': 'center' },
        'Title'    : { 'width': 6 },
        'Genre'    : { 'width': 3 },
        'Plot'     : { 'width': 6 },
        'Actors'   : { 'width': 6 },
        'Director' : { 'width': 5 }
      }
    }

    self._metadata_movies_additional = {
      'fields': {
        'Rated'    : { 'width': 2,   'align': 'center' },
        'Runtime'  : { 'width': 2.5, 'align': 'center' },
        'Metascore': { 'width': 2,   'align': 'center' }
      }
    }

This modular approach makes it easier to handle different sheets, even when the column structures vary.

In practice, I’ve found that manually updating, long column references every time the source sheet changes is exhausting. Automating this with relative positioning is far more efficient.

Configuration: Merge Metadatas

Combining Block Configurations

To bring everything together, we merge all column blocks into a single metadata collection.

Calc Macro: Step Four: Merge Metadatas

class FormatterTabularMovies(FormatterTabularData):

  def _merge_metadatas(self) -> None:
    # Columns:   A, H,  L
    self._gaps = [0, 7, 11]

    self._metadatas = [{
      'col-start'     : 'B',
      **self._metadata_movies_base
    }, {
      'col-start'     : 'I',
      **self._metadata_movies_additional
    }]

The focus here is on keeping track of starting points for each block. In this case, columns B and I mark the beginning of each section.

By specifying just the key positions, we eliminate the need for tedious letter-by-letter adjustments.

Formatting: Set Columns Formats

Implementing Unified Configuration

Let’s revisit the _set_columns_format() method in the descendant class. How do we integrate unified configuration into the process?

We can break it down into two main stages:

  • Outer loop: Iterates through unified metadata blocks.
  • Inner loop: Applies formatting to each field within those blocks.

Stage One: Outer Loop

The outer loop iterates over each metadata block stored in _metadatas.

Calc Macro: Step Four: Format Columns

class FormatterCommon(FormatterBase):
  ...

  def _set_columns_format(self) -> None:
    columns = self._sheet.Columns

    # Alignment mapping
    alignment_map = {
        'left'  : LEFT,  'center': CENTER, 'right' : RIGHT }

    for metadata in self._metadatas:
      start_letter = metadata['col-start']

      pairs = metadata['fields'].items()
      for pair_index, (field, data) in enumerate(pairs, start=1):

At this stage, the method prepares for the inner loop, by extracting fields and starting positions. The start_letter indicates the beginning of each block.

The outer loop effectively creates a foundation, for iterating through each field within the block.

Stage Two: Inner Loop – Applying Formats

Now let’s focus on the inner loop, where the real formatting happens.

Calc Macro: Step Four: Format Columns

      for pair_index, (field, data) in enumerate(pairs, start=1):
        letter = self._get_relative_column_letter(
          start_letter, pair_index)
        width  = data['width'] * 1000
        align  = data.get('align')

        col_index = self._column_letter_to_index(letter)
        column = columns.getByIndex(col_index)
        column.Width = width

        ...

In this part:

  • Column letters are dynamically calculated using _get_relative_column_letter().
  • Column widths are set by multiplying the configured width by 1000.
  • Alignment settings are fetched, but not yet applied.

The variable col_index points to the correct column, and the width adjustment happens directly on the column object.

Formatting Cell Ranges

Next, we define the cell range, and apply alignment and number formats, if specified.

      for pair_index, (field, data) in enumerate(pairs, start=1):
        ...

        start_row = 3
        end_row = self._max_row
        cell_range = self._sheet.getCellRangeByPosition(
          col_index, start_row, col_index, end_row)

        if align in alignment_map:
           cell_range.HoriJustify = alignment_map[align]

        if cell_format := data.get('format'):
           cell_range.NumberFormat = self._get_number_format(cell_format)

Here’s what happens:

  • A cell range is created from row 3 to the maximum row in the sheet.
  • Horizontal alignment is applied if specified in the metadata.
  • If a number format is provided, the method _get_number_format() is called to fetch the format and apply it to the column.

Required Helper Methods

To make this work, we rely on three essential helper methods:

  • _column_letter_to_index(): Converts column letters to indices.
  • _get_relative_column_letter(): Finds column letters relative to a starting point.
  • _get_number_format(): Retrieves or creates number formats as needed.

We’ve already covered these helpers in previous sections, aking this part of the process straightforward to implement.

By combining these pieces, the entire column formatting process becomes highly adaptable. Nomore hardcoding individual columns.

As a summary, we have set:

  • Width
  • Alignment
  • Number Format

Sheet Result

Here’s a preview of the final result after applying the configuration:

Calc Sheet: Step Four: Output

Console Logging.

No additional configuration or surprises for this step. Just smooth execution.

>>> tabular_single_movies()
 * Rearranging Columns
   - Insert Gap: A
   - Insert Gap: H
   - Insert Gap: L
 * Setting Rows Width
 * Formatting Columns
 * Finished

The console logs give a clear, step-by-step breakdown of what’s happening behind the scenes. From inserting column gaps to finalizing the column formatting, the output reflects the unified configuration at work. Efficient and predictable.

No hidden traps or unexpected behaviors. Just the macro doing its job.


05: Additional Helper, Constant and Class

Let’s keep things organized.

Helper Method:

  • _format_cell_rectangle().

Color scale constant:

  • blueScale, tealScale, amberScale, brownScale.
  • clBlack.

Border Type:

  • lfNone, lfBlack, lfGray.

Helper: Format Cell Rectangle.

No surprises here. This helper is exactly the same as the one used in the simple configuration.

Constant: Color Scale

Same as before. The color constants are carried over without modification.

Calc Macro: Step Five: Color Scale

Class: Border Format Manager

To keep the configuration concise and readable, let’s avoid verbose references like self.lfNone. Instead, we’ll simplify by defining a BorderFormatManager class that returns border formats directly as variables.

Calc Macro: Step Five: Border Format

class BorderFormatManager:
  def create_line_format_none(self) -> BorderLine2:
    lineFormatNone = BorderLine2()
    lineFormatNone.LineStyle = BorderLineStyle.NONE
    return lineFormatNone

  def create_line_format_black(self) -> BorderLine2:
    ...

  def create_line_format_gray(self) -> BorderLine2:
    .

Now, instead of typing out the full method call every time, we can initialize the border formats once, and reference them as needed:

bfm = BorderFormatManager()
lfNone  = bfm.create_line_format_none()
lfBlack = bfm.create_line_format_black()
lfGray  = bfm.create_line_format_gray()

This keeps the code clean and reduces repetition. Plus, it makes border configurations feel, more like natural variables than method calls.

Import State

To support the new border formats and helpers, a few extra imports are required from com.sun.star:

from abc import ABC, abstractmethod

from com.sun.star.sheet import XSpreadsheetDocument
from com.sun.star.util  import XNumberFormats

from com.sun.star.\
  awt.FontWeight import BOLD
from com.sun.star.\
  table.CellHoriJustify import LEFT, CENTER, RIGHT
from com.sun.star.\
  table import BorderLine2, BorderLineStyle

Nothing fancy, just the essentials to get everything working smoothly.


05: Step Five

Step Overview

In this section, we focus on expanding the unified configuration with a few key additions:

  • New Topics (Unified Configuration):

    • Class: BorderFormatManager
    • Formatting Procedure: Add merged titles (multiple block of columns)
    • Formatting Procedure: Set merge title (refactored for one block)
    • Formatting Procedure: Add merged titles post (Hook)
  • Revisited Topics (Simple Configuration):

    • Helper: Format Cell Rectangle
    • Predefined Constant: Color Scale (GMC)
    • Predefined Constant: Line Format
    • Formatting Procedure: Add merged title

Macro Source

The full macro source for this section can be found here:

Class Diagram

Focus on the highlights, the new additions stand out.

In the updated diagram, you’ll notice a few key elements:

  • New Abstract Method: _add_merged_titles()
  • New Protected Helper: format_cell_rectangle() for formatting cells.
  • Color Scale Box: This holds the predefined color constants for easy reference.

Class Diagram: Formatting: Unified Configuration: Step 05

Unlike the simple configuration class, the core of prepare_sheet() remains unchanged, since border-related configurations are separated for clarity.

Abstract Method

One abstract method named _add_merged_titles() is introduced at this stage:

  @abstractmethod
  def _add_merged_titles(self) -> None:
    pass

This sets the foundation for handling merged title formatting in descendant classes.

Basic Flow

With this new method in place, we extend the existing flow to incorporate, the merged title formatting.with add_merged_title().

Calc Macro: Step Five: Basic Flow

  def __format_one_sheet(self) -> None:
    ...

    # Apply Header Settings
    print(' * Formatting Header')
    self._add_merged_titles()

The actual implementation of _add_merged_titles(), will be handled in the descendant class, keeping the base class lean and abstract.

Configuration: Init Metadatas

Instance Variables

In line with the unified configuration approach, we consolidate metadata initialization into a structured format. This starts with the fields dictionary, defining column letters, widths, and optional alignments. On top of that, we introduce titles to manage block-level configurations.

Calc Macro: Step Five: Init Metadatas

class FormatterTabularData(FormatterTabular):
  ...

  # Unified Configuration
  def _init_metadatas(self) -> None:
    self._metadata_movies_base = {
      'fields': {...},
      'titles': [{...}]
    }

    self._metadata_movies_additional = {
      'fields': {...},
      'titles': [{...}]
    }

Each section represents a discrete block of columns, simplifying layout management.

Configuration: Column Blocks

Now, let’s break down the individual column blocks.

Base Column Block

This block covers the core movie attributes: year, title, genre, plot, and so on.

Calc Macro: Step Five: Init Metadatas

    self._metadata_movies_base = {
      'fields': {
        'Year'     : { 'width': 1.5, 'align': 'center' },
        'Title'    : { 'width': 6 },
        'Genre'    : { 'width': 3, },
        'Plot'     : { 'width': 6, },
        'Actors'   : { 'width': 6, },
        'Director' : { 'width': 5, }
      },

      'titles': [{ 
        'col-start-id' : 1, 'col-end-id' : 6, 'text' : 'Base Movie Data', 
        'bg' : blueScale[3], 'fg' : clBlack                    
      }]
    }

Additional Column Block

The additional block handles auxiliary data: ratings, runtime, and metascore.

    self._metadata_movies_additional = {
      'fields': {
        'Rated'    : { 'width': 2,   'align': 'center' },
        'Runtime'  : { 'width': 2.5, 'align': 'center' },
        'Metascore': { 'width': 2,   'align': 'center' }
      },
      'titles': [{ 
        'col-start-id' : 1, 'col-end-id' : 3, 'text' : 'Additional Data', 
        'bg' : tealScale[3], 'fg' : clBlack                    
      }]
    }

Configuration: Titles

You might wonder. Why use a list for titles?

      'titles': [{ 
        'col-start-id' : 1, 'col-end-id' : 6, 'text' : 'Base Movie Data', 
        'bg' : blueScale[3], 'fg' : clBlack                    
      }]

The answer lies in flexibility. This structure allows multiple title rows within the same block, like this:

      'titles': [{ 
        'col-start-id' : 1, 'col-end-id' : 3, 'text' : 'Base Movie Data', 
        'bg' : blueScale[3], 'fg' : clBlack                    
      }, { 
        'col-start-id' : 4, 'col-end-id' : 6, 'text' : 'Other Movie Data', 
        'bg' : tealScale[3], 'fg' : clBlack                    
      }]

This modular approach makes it easy, to adjust titles or split sections without rewriting large chunks of code.

Configuration: Merge Metadatas

For now, no changes are needed in the metadata merging process. Everything flows seamlessly until the end of this tutorial.

Formatting: Add Merged Title

Inner Loop

How do we implement unified configuration for merged titles?

The process involves two main loop stages:

  • Outer Loop: Iterates over unified metadata blocks.
  • Inner Loop: Iterates over:
    • Fields: Handled separately by set_columns_format().
    • Titles: Directly relevant to merged title formatting.

Calc Macro: Step Five: Add Merged Title

In this method, we only leverage the titles portion of the inner loop, as the fields logic resides in set_columns_format().

Now, let’s break down _add_merged_titles(), which calls the helper method __set_merged_title().

  def _add_merged_titles(self) -> None:
    for metadata in self._metadatas:
      self.__set_merged_title(metadata)

    # Call the hook method (default does nothing)
    self._add_merged_titles_post()

Hook for Extensibility

I’ve added a hook method _add_merged_titles_post() for subclasses to override if necessary.

Most of the time, this hook stays untouched. However, in edge cases where additional formatting, or adjustments are required. This hook proves valuable.

  def _add_merged_titles_post(self) -> None:
    pass

This setup ensures the base class maintains simplicity, while descendant classes can introduce custom behavior when needed.

Without modifying core logic.

Solving Hardcoded

Remember those hardcoded lines from earlier?

    self._sheet['B2:BC3'].HoriJustify = CENTER
    self._sheet['B2:BC2'].CharWeight  = BOLD 

Instead of relying on fixed ranges, we can dynamically calculate them based on the titles configuration. This approach keeps the code flexible and adaptable to changes in metadata.

Helper: Set Merged Title

To avoid cluttering _add_merged_titles() with too much detail, I extracted the logic into a private method named __set_merged_title().

Calc Macro: Step Five: Set Merged Title

This method is essentially a refactored, and cleaner version of the original logic.

  def __set_merged_title(self, metadata: dict[str, any]) -> None:
    start_letter = metadata['col-start']

    for title in metadata['titles']:
      col_letter_start = self._get_relative_column_letter(
          start_letter, title['col-start-id'])  
      col_letter_end   = self._get_relative_column_letter(
          start_letter, title['col-end-id'])

      cell = self._sheet[f"{col_letter_start}2"]
      cell.String        = title['text']
      cell.CellBackColor = title['bg']
      cell.CharColor     = title['fg']

Default Formatting Behavior

The merged header defaults to:

  • Bold text (BOLD)
  • Center-aligned (CENTER)
  • Black border

Here’s the complete version, including the formatting and merging logic:

  def __set_merged_title(self, metadata: dict[str, any]) -> None:
    start_letter = metadata['col-start']

    for title in metadata['titles']:
      ...

      pos = self._column_letter_to_index(col_letter_start)
      self._format_cell_rectangle(
        1, 1, pos, pos, lfBlack)

      merge_address = f"{col_letter_start}2:{col_letter_end}2"
      self._sheet[merge_address].merge(True)
      self._sheet[merge_address].CharWeight = BOLD

      header_address = f"{col_letter_start}2:{col_letter_end}3"
      self._sheet[header_address].HoriJustify = CENTER

That’s It!

This refactor eliminates brittle hardcoding, ensuring the code adapts seamlessly to any column configuration

Sheet Result

The final output for this step will appear as follows:

Calc Sheet: Step Five: Output

Console Logging.

For this step, the console output will look like this:

>>> tabular_single_movies()
 * Rearranging Columns
   - Insert Gap: A
   - Insert Gap: H
   - Insert Gap: L
 * Setting Rows Width
 * Formatting Columns
 * Formatting Header
 * Finished

Sometimes, we may not configure everything correctly. By reviewing the console logs, we can quickly debug what went wrong.

Additionally, for heavy processing tasks, using logs like this is more efficient, than constantly watching htop, and seeing a CPU core hogging 100%.


What is Next 🤔?

With the design pattern in place, we can now proceed to colorize the header. And also add borders for both the header and the data.

Feel free to continue reading [ Formatter - Unified Config - Three ].