How do I make coloured rows in ColumnView?

I’ve read about how coloured rows in GTK are done, but this is only for the old Gtk.TreeView and Gtk.ListStore.

So I looked for a way to implement it in GTK4, then I read in here that it’s a complex functionality.

But in response to the last post in that thread, I really need to ask how I can assign and remove CSS based on some logic? There’s no option in Gtk.ColumnViewRow to add CSS, and the Gtk.ColumnView class doesn’t have background-color listed as a CSS option.

How do I make the rows with individual colours if there’s no option to do so?

1 Like

In short:

Each visible cell in an Gtk.ColumnView is representated by an widget generated by an factory [1]. So, you can use the normal widget methods to add or remove CSS classes from it in code [2]. Then, you can style a cell in your application CSS stylesheet.

So, if you were, for example, using the Gtk.SignalListItemFactory to populate your grid, you would add the css classes on the bind signal activation, and remove them on the unbind signal.


  1. GtkColumnView – GTK Development Blog ↩︎

  2. See methods Gtk.Widget.add_css_class and Gtk.Widget.remove_css_class. ↩︎

I just tried it, it didn’t work, it gave me assertion failed messages, anything I need to fix to get it to work?

To my understanding, the Gtk.ColumnViewCell itself only is a data structure used by Gtk.ColumnView to build itself.

The widget that is actually displayed, and therefore can be styled, would be its child.

See this property of Gtk.ColumnViewCell:

So you need to call the add_css_class method on the child of the cell.

I tried that too, in fact I even tried using Gtk.Box as the child widget of Gtk.ColumnViewCell:child property, and set up the AddCssClass("background-color: #FF5733"), just to test it out and make sure it works, and nope, nothing.

Is there anything else I need to do to make it work? I even tried adding AddCssClass("background-image: none").

Looking back at the code, it seems you’ve misinterpreted what the add_css_class method [1] does.

This method is used to add a CSS class to the widget, which then can be referenced in your CSS stylesheet.

As an example:
If I were to set custom-background as a css class, I could then set the style for it using this CSS snippet:

.custom-background {
    background-color: red;
}

So, you need to set a name in the method and then set the styling in your CSS stylesheet.

If you use libadwaita, adding your own stylesheet is as simple as adding a style.css to your application resources [2]. In the base GTK, I think you need to add the CSS file yourself using a Gtk.CssProvider [3].

If you want to dynamically change row colors at runtime, I think you can generate a CSS at runtime and use that. But if that is something you’re planning, I would check how applications like GNOME Text Editor accomplish this.


  1. Gtk.Widget.add_css_class ↩︎

  2. Adw.Application ↩︎

  3. Gtk.CssProvider ↩︎

Yeah… It’s not working.

I tried with CssProvider.LoadFromString(), since I want to do it all at runtime without having to write an entirely new file.

    private static void OnBindEventTypeText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args)
    {
        if (args.Object is not Gtk.ListItem listItem)
        {
            return;
        }

        if (listItem.Child is not Gtk.Box box) return;
        if (listItem.Item is not TrackViewer userData) return;
        if (box.GetFirstChild() is not Gtk.Label label) return;
        if (userData._eventType is null) return;

        var cssProvider = Gtk.CssProvider.New();
        cssProvider.LoadFromString(
            ".custom-background {" +
            "background-image: none;" +
            "background-color: red;" +
            "}"
        );
        box.AddCssClass("custom-background");

        label.SetText(userData._eventType);
    }

This doesn’t work at all. How do I use LoadFromString correctly?

I even tried using Gtk.DrawingArea as an alternative, but it doesn’t render at all in Gtk.ColumnView. Is there something blocking DrawingArea from being used in Gtk.ColumnView?

1 Like

With the code you’ve shown here, you just created the GtkCssProvider, without adding it to anything.

You also need to add it, using Gtk.StyleContext.add_provider_for_display for it to be used.

Also, it should be noted that CSS in GTK should be applied globally. So the whole CSS provider code should be in your application class, not your widget.

1 Like

Ah I see, makes sense, now it works!

The only issues now is that the background color doesn’t cover the entire cell, and when the row is selected, the color still covers the highlighted selection.

    private static void OnBindEventTypeText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args)
    {
        if (args.Object is not Gtk.ListItem listItem)
        {
            return;
        }

        if (listItem.Child is not Gtk.Box box) return;
        if (listItem.Item is not TrackViewer userData) return;
        if (box.GetFirstChild() is not Gtk.Label label) return;
        if (userData._eventType is null) return;

        var hexCode = $"{userData._commandColor.R:X2}{userData._commandColor.G:X2}{userData._commandColor.B:X2}".ToLower();

        var eventClass = $"{userData._eventType}".ToLower().Replace(' ', '-');

        var cssProvider = Gtk.CssProvider.New();
        cssProvider.LoadFromString(
            $".{eventClass}" + " {" +
            "background-image: none;" +
            $"background-color: #{hexCode};" +
            "}"
        );
        box.AddCssClass($"{eventClass}");

        Gtk.StyleContext.AddProviderForDisplay(box.GetDisplay(), cssProvider, 0);

        label.SetText(userData._eventType);
    }

Is there any way to fill the entire cell with the background color and disable the background color when the row is selected?

Edit: Another issue! Why does the colors get broken when there’s a lot of ColumnView entries? It’s supposed to only apply the color per-event type, but I see it applying to the wrong events (eg. Note is colored red instead of green) when scrolling all the way down when there’s a lot of entries. Here’s what I mean:

How do I fix that issue?

Hi,

For the row background, you can use the css “row” node. So in your CSS file (for example):

row {
background-color: white;
}

row:selected {
background-color: grey;
}

This will impact every “row” in your project, so if needed, you might want to use a class or an id on your listview/columnview, and use the child combinator to access the row child. For example :

columnview.myclass > listview >row {
background-color: red;
}

columnview.myclass > listview >row:selected{
background-color: yellow;
}

Unfortunately, there is no way to add a class directly to a row as far as I know.

As CodedOre said, you need to put your css provider etc outside you binding function. At the time of binding, you should only add/bind your css class to the widget.

1 Like

There are probably some margins on the cell.
I think it would be best to check the grid and its CSS with the inspector.

You probably loading too many CssProvider, as you still create them in the bind signal.

You should only use one provider for all additional CSS of the grid, updating this in case you need to update the CSS.

Okay, it didn’t work, I probably missed something important, I wonder what it is?

Bind code:

    private void OnBindEventTypeText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args)
    {
        if (args.Object is not Gtk.ListItem listItem)
        {
            return;
        }

        if (listItem.Child is not Gtk.Box box) return;
        if (listItem.Item is not TrackViewer userData) return;
        if (box.GetFirstChild() is not Gtk.Label label) return;
        if (userData._eventType is null) return;

        var eventClass = $"{userData._eventType}".ToLower().Replace(' ', '-');

        box.AddCssClass($"{eventClass}");

        label.SetText(userData._eventType);
    }

Outside of bind code, CSS generation:

    public void ReloadColumnEntries()
    {
        if (_model.GetNItems() is not 0)
            _model.RemoveAll();

        if (Engine.Instance is null) return;
        if (Engine.Instance.Player.LoadedSong is null) return;

        TrackData = new TrackViewer[Engine.Instance.Player.LoadedSong.Events[_trackDropDown!.Selected]!.Count];
        string cssCode = "";
        List<string> eventsUsed = [];
        foreach (var trackEvent in Engine.Instance.Player.LoadedSong.Events[_trackDropDown.Selected]!)
        {
            var hexCode = $"{trackEvent.Command.Color.R:X2}{trackEvent.Command.Color.G:X2}{trackEvent.Command.Color.B:X2}".ToLower();

            var eventClass = $"{trackEvent.Command.Label}".ToLower().Replace(' ', '-');

            var hslColor = new HSLColor(trackEvent.Command.Color);
            var textColor = hslColor.Lightness <= 0.6 && hslColor.R <= 0.7 && hslColor.G <= 0.7 ? "white" : "black";
            if (!eventsUsed.Contains(eventClass))
            {
                cssCode += $"columnview.{eventClass} > listview > row" + " {" +
                    "background-image: none;" +
                    $"background-color: #{hexCode};" +
                    $"color: {textColor};" +
                "} ";
                cssCode += $"columnview.{eventClass} > listview > row:selected" + " {" +
                    "background-image: none;" +
                    $"background-color: blue;" +
                    $"color: white;" +
                "} ";
                eventsUsed.Add(eventClass);
            }
        }
        _cssProvider = Gtk.CssProvider.New();
        _cssProvider.LoadFromString(cssCode);
        Gtk.StyleContext.AddProviderForDisplay(ColumnView!.GetDisplay(), _cssProvider, 0);
        int i = 0;
        foreach (var trackEvent in Engine.Instance.Player.LoadedSong.Events[_trackDropDown.Selected]!)
        {
            var numTicks = new long[trackEvent.Ticks.Count];
            int t = 0;
            foreach (var ticks in trackEvent.Ticks)
            {
                numTicks[t++] = ticks;
            }
            TrackData[i++] = new TrackViewer(trackEvent.Command, trackEvent.Offset, numTicks);
        }

        foreach (var data in TrackData!)
        {
            _model.Append(data);
        }
    }

You set your CSS class to the wrong widget.

columnview.{eventClass} > listview > row will check for the CSS class on the ColumnView widget, or the entire grid, not the row in question.

So, you need to check for the CSS class at the row widget:

columnview > listview > row.{eventClass} {
  // ...
}

Also, in your current code, reloading the column entries doesn’t seem to remove existing CssProviders, so you could still end up with multiple CssProviders.

Even that didn’t work

    public void ReloadColumnEntries()
    {
        if (_model.GetNItems() is not 0)
            _model.RemoveAll();

        if (Engine.Instance is null) return;
        if (Engine.Instance.Player.LoadedSong is null) return;

        TrackData = new TrackViewer[Engine.Instance.Player.LoadedSong.Events[_trackDropDown!.Selected]!.Count];
        string cssCode = "";
        List<string> eventsUsed = [];
        foreach (var trackEvent in Engine.Instance.Player.LoadedSong.Events[_trackDropDown.Selected]!)
        {
            var hexCode = $"{trackEvent.Command.Color.R:X2}{trackEvent.Command.Color.G:X2}{trackEvent.Command.Color.B:X2}".ToLower();

            var eventClass = $"{trackEvent.Command.Label}".ToLower().Replace(' ', '-');

            var hslColor = new HSLColor(trackEvent.Command.Color);
            var textColor = hslColor.Lightness <= 0.6 && hslColor.R <= 0.7 && hslColor.G <= 0.7 ? "white" : "black";
            if (!eventsUsed.Contains(eventClass))
            {
                cssCode += $"columnview > listview > row.{eventClass}" + " {" +
                    "background-image: none;" +
                    $"background-color: #{hexCode};" +
                    $"color: {textColor};" +
                "} ";
                cssCode += $"columnview > listview > row.{eventClass}:selected" + " {" +
                    "background-image: none;" +
                    $"background-color: blue;" +
                    $"color: white;" +
                "} ";
                eventsUsed.Add(eventClass);
            }
        }
        if (_cssProvider is null)
        {
            _cssProvider = Gtk.CssProvider.New();
            _cssProvider.LoadFromString(cssCode);
            Gtk.StyleContext.RemoveProviderForDisplay(ColumnView!.GetDisplay(), _cssProvider);
            Gtk.StyleContext.AddProviderForDisplay(ColumnView!.GetDisplay(), _cssProvider, 0);
        }
        int i = 0;
        foreach (var trackEvent in Engine.Instance.Player.LoadedSong.Events[_trackDropDown.Selected]!)
        {
            var numTicks = new long[trackEvent.Ticks.Count];
            int t = 0;
            foreach (var ticks in trackEvent.Ticks)
            {
                numTicks[t++] = ticks;
            }
            TrackData[i++] = new TrackViewer(trackEvent.Command, trackEvent.Offset, numTicks);
        }

        foreach (var data in TrackData!)
        {
            _model.Append(data);
        }
    }
    private void OnBindEventTypeText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args)
    {
        if (args.Object is not Gtk.ListItem listItem)
        {
            return;
        }

        if (listItem.Child is not Gtk.Box box) return;
        if (listItem.Item is not TrackViewer userData) return;
        if (box.GetFirstChild() is not Gtk.Label label) return;
        if (userData._eventType is null) return;

        var eventClass = $"{userData._eventType}".ToLower().Replace(' ', '-');

        box.AddCssClass($"{eventClass}");

        label.SetText(userData._eventType);
    }

Is there something else I’m missing?

I don’t see any obvious errors in the code snippet.

I’d say you could use the GTK inspector and double-check the CSS properties for a row widget. Maybe there you can find a discrepancy?

Also check if GTK shows any related warning.

It doesn’t show any warnings (except for the one about GtkPopoverMenuBar widget, but that’s unrelated to this window since it occurs on startup), but I did find something interesting in the inspector:

Since AddCssClass(eventClass) was applied to the child widget, you’d think it would also apply it to the row as well, but as you can see in the screenshot, it doesn’t, which is really odd.

But when I add the class manually in the inspector, this happens:

As you can see, the CSS and provider works, but the CSS class doesn’t get added to the widget itself.

So I really need to ask, is there any way I can add the class to the list of css-classes? I’ve even tried this:

    private void OnBindEventTypeText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args)
    {
        if (args.Object is not Gtk.ListItem listItem)
        {
            return;
        }
        
        if (listItem.Item is not TrackViewer userData) return;
        var eventClass = $"{userData._eventType}".ToLower().Replace(' ', '-');

        listItem.Child.AddCssClass($"{eventClass}");

        if (listItem.Child is not Gtk.Box box) return;
        if (box.GetFirstChild() is not Gtk.Label label) return;
        if (userData._eventType is null) return;

        label.SetText(userData._eventType);
    }

And it still doesn’t work…

Well, it seems the CSS selector is a bit off then. I kinda expected that to be the issue.

The best way would be to change the CSS to select the box widget that has the CSS class.
Tip: Use the CSS tab in the GTK inspector to quickly prototype the correct selector.

Alternatively, you could try to get the row widget in your code with Gtk.Widget.get_parent to apply the CSS class to the row widget. Though I’m nor sure this is the best solution…

As I tried this out:

    public void ReloadColumnEntries()
    {
        if (_model.GetNItems() is not 0)
            _model.RemoveAll();

        if (Engine.Instance is null) return;
        if (Engine.Instance.Player.LoadedSong is null) return;

        TrackData = new TrackViewer[Engine.Instance.Player.LoadedSong.Events[_trackDropDown!.Selected]!.Count];
        string cssCode = "";
        List<string> eventsUsed = [];
        foreach (var trackEvent in Engine.Instance.Player.LoadedSong.Events[_trackDropDown.Selected]!)
        {
            var hexCode = $"{trackEvent.Command.Color.R:X2}{trackEvent.Command.Color.G:X2}{trackEvent.Command.Color.B:X2}".ToLower();

            var eventClass = $"{trackEvent.Command.Label}".ToLower().Replace(' ', '-');

            var hslColor = new HSLColor(trackEvent.Command.Color);
            var textColor = hslColor.Lightness <= 0.6 && hslColor.R <= 0.7 && hslColor.G <= 0.7 ? "white" : "black";
            if (!eventsUsed.Contains(eventClass))
            {
                cssCode += $"columnview > listview > row > cell > box.{eventClass}" + " {" +
                    "background-image: none;" +
                    $"background-color: #{hexCode};" +
                    $"color: {textColor};" +
                "} ";
                cssCode += $"columnview > listview > row:selected > cell > box.{eventClass}" + " {" +
                    "background-image: none;" +
                    $"background-color: blue;" +
                    $"color: white;" +
                "} ";
                eventsUsed.Add(eventClass);
            }
        }
        if (_cssProvider is null)
        {
            _cssProvider = Gtk.CssProvider.New();
            _cssProvider.LoadFromString(cssCode);
            Gtk.StyleContext.RemoveProviderForDisplay(ColumnView!.GetDisplay(), _cssProvider);
            Gtk.StyleContext.AddProviderForDisplay(ColumnView!.GetDisplay(), _cssProvider, 0);
        }
        int i = 0;
        foreach (var trackEvent in Engine.Instance.Player.LoadedSong.Events[_trackDropDown.Selected]!)
        {
            var numTicks = new long[trackEvent.Ticks.Count];
            int t = 0;
            foreach (var ticks in trackEvent.Ticks)
            {
                numTicks[t++] = ticks;
            }
            TrackData[i++] = new TrackViewer(trackEvent.Command, trackEvent.Offset, numTicks);
        }

        foreach (var data in TrackData!)
        {
            _model.Append(data);
        }
    }

It produced the same issue I had before, but this time without multiple providers:

I tried this too, using the Gtk.Internal classes, which GirCore uses for arbitrary code

    private void OnBindEventTypeText(Gtk.SignalListItemFactory sender, Gtk.SignalListItemFactory.BindSignalArgs args)
    {
        if (args.Object is not Gtk.ListItem listItem)
        {
            return;
        }

        if (listItem.Child is not Gtk.Box box) return;
        if (listItem.Item is not TrackViewer userData) return;
        if (box.GetFirstChild() is not Gtk.Label label) return;
        if (userData._eventType is null) return;

        var eventClass = $"{userData._eventType}".ToLower().Replace(' ', '-');

        if (listItem is Gtk.ColumnViewCell cell)
        {
            var parent = Gtk.Internal.Widget.GetParent(cell.Handle.DangerousGetHandle());
            Gtk.Internal.Widget.AddCssClass(parent, GLib.Internal.NonNullableUtf8StringOwnedHandle.Create($"{eventClass}"));
        }

        label.SetText(userData._eventType);
    }

And it didn’t work, I just got assertion failed critical messages coming up, as seen here:

Am I even doing this correctly?

Then the suggestion I gave was just a bad one.

My thinking was that Gtk.Widget.get_parent, in theory, should give you the widget that contains the cell widget, e.g. the row.

But it doesn’t seem GTK operates in that way in a ColumnView.

To be honest, I don’t have any good idea currently. I don’t know the inner workings of the ColumnView enough to give a definitive answer. Maybe some of the GTK developers could give you a pointer.

1 Like

About your issues in your code with the first suggestion:

Do you remove the CSS classes from the rows in the unbind method?

May be an issue since GTK reuses the rows, so if you don’t remove then during unbind they pile up on the widget.