Eto - Custom Scrollable (Drawable) - Help Adding Layout

Hello everyone,

I’m about to start working on a custom Scrollable to control the visual styling of it.

Something visually similar to the discourse (these forums) scrollable like this:

image

In drawable OnPaint I will create a pill shape for the “handle” of the scrollable and use a single line for the background “overall length” of the scrollable.

My question specifically is which parent do I inherit from to expose the “OnMouseEnter” “OnMouseLeave” “OnMouseWheel” etc. events? Since Drawable inherits from Eto.Forms.Control is this all I need? Or do I need to inherit from Eto.Forms.Scrollable somehow?

I’m guessing the scrollable handle is dynamically set based on the amount of content that the scrollable contains?

Can I visually change the “handle” “up/down arrows” and “scroll bar/background line” of a scrollable while still keeping the functionality of using it as a layout “container” to house other controls?

I’m just a little confused on where to start and what overrides what…
I could not find any examples of this online or on the forums here though I’ve seen a couple users here and there showcasing custom scrollable graphics.

Here’s my testing mockup code I’m working on:

import Rhino
import Eto

import Rhino
import Eto


class CustomScrollable(Eto.Forms.Drawable):
    def __init__(self):
        super(CustomScrollable, self).__init__()
        self.Size = Eto.Drawing.Size(300, 200)
        self.content_height = 0
        self.scroll_offset = 0
        self.mouse_down = False
        self.start_mouse_y = 0
        self.start_scroll_offset = 0
        self.content = []
        self.scroll_speed = 8  # Scroll speed factor

    def set_content(self, content):
        self.content = content
        self.content_height = len(content) * 25  # Adjust as needed for item height
        self.Invalidate()

    def OnPaint(self, e):
        try:
            e.Graphics.Clear(Eto.Drawing.Colors.White)
            y_offset = -self.scroll_offset

            y = y_offset
            for item in self.content:
                e.Graphics.DrawText(Eto.Drawing.Font("Arial", 12), Eto.Drawing.Colors.Black, 10, y, item)
                y += 25  # Adjust as needed for line spacing

        except Exception as ex:
            print(ex)

    def OnMouseDown(self, e):
        self.mouse_down = True
        self.start_mouse_y = e.Location.Y
        self.start_scroll_offset = self.scroll_offset

    def OnMouseUp(self, e):
        self.mouse_down = False

    def OnMouseMove(self, e):
        if self.mouse_down:
            delta = e.Location.Y - self.start_mouse_y
            self.scroll_offset = self.start_scroll_offset - delta
            self.scroll_offset = max(0, min(self.scroll_offset, self.content_height - self.Height))
            self.Invalidate()

    def OnMouseWheel(self, e):
        self.scroll_offset -= e.Delta.Height * self.scroll_speed
        self.scroll_offset = max(0, min(self.scroll_offset, self.content_height - self.Height))
        self.Invalidate()

class MainForm(Eto.Forms.Form):
    def __init__(self):
        super().__init__()

        # Set Form General Settings
        self.Title = "Main Form"
        self.Size = Eto.Drawing.Size(300, 300)
        self.Resizable = False
        self.MovableByWindowBackground = False

        self.CreateUI()  # Call Initially To Create UI

    def CreateUI(self):
        # Create Pixel Layout For Background Graphics
        self.layout = Eto.Forms.PixelLayout()

        # Setup Bitmap For Background Graphics To Be Drawn On
        pixelformat = Eto.Drawing.PixelFormat.Format32bppRgba
        bitmap = Eto.Drawing.Bitmap(self.Size, pixelformat)
        self.graphics = Eto.Drawing.Graphics(bitmap)

        # Create a stack layout to hold the search bar and search results
        self.stack_layout = Eto.Forms.StackLayout()
        self.stack_layout.Orientation = Eto.Forms.Orientation.Vertical

        test_header = Eto.Forms.Label()
        test_header.Text = "Test Scrollable Header"

        example_scrollable_content = [
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small"
        ]

        # Create the custom scrollable area
        self.custom_scrollable = CustomScrollable()
        self.custom_scrollable.Width = self.Width - 40
        self.custom_scrollable.Height = self.Height - 100
        self.custom_scrollable.BackgroundColor = Eto.Drawing.Colors.Transparent
        self.custom_scrollable.set_content(example_scrollable_content)

        # Add the title header bar to the stack layout
        self.stack_layout.Items.Add(Eto.Forms.StackLayoutItem(test_header))
        self.stack_layout.Items.Add(Eto.Forms.StackLayoutItem(self.custom_scrollable))

        # Add the stack layout to the pixel layout and set its bounds
        self.layout.Add(self.stack_layout, Eto.Drawing.Point(5, 5))

        # Set the content of the form to be the pixel layout
        self.Content = self.layout


def EstablishForm():
    main_form = MainForm()
    main_form.Owner = Rhino.UI.RhinoEtoApp.MainWindow
    main_form.Show()


if __name__ == "__main__":
    EstablishForm()


Thank you all for any leads!

EDIT:

Okay I’m getting closer and have something mostly working, however, sometimes it crashes when using the mouse to “drag scroll” and sometimes it crashes using the mouse wheel scroll, other times it works fine so I’m having a hard time debugging the issue.

Here’s one of the errors I get in the Rhino Crash Dump:

[ERROR] FATAL UNHANDLED EXCEPTION: System.NullReferenceException: Object reference not set to an instance of an object.
   at Eto.Wpf.Forms.WpfFrameworkElement`3.HandlePreviewMouseWheel(Object sender, MouseWheelEventArgs e) in D:\BuildAgent\work\dujour\src4\DotNetSDK\Eto\src\Eto.Wpf\Forms\WpfFrameworkElement.cs:line 545

Here’s my updated code:

#! python3

import Rhino
import Eto


class CustomScrollable(Eto.Forms.Drawable):
    def __init__(self):
        super(CustomScrollable, self).__init__()
        self.Size = Eto.Drawing.Size(300, 200)
        self.content_height = 0
        self.scroll_offset = 0
        self.mouse_down = False
        self.start_mouse_y = 0
        self.start_scroll_offset = 0
        self.content = []
        self.scroll_speed = 8  # Scroll speed factor

    def set_content(self, content):
        self.content = content
        self.content_height = len(content) * 25  # Adjust as needed for item height
        self.Invalidate()

        self.scroll_pen = Eto.Drawing.Pen(Eto.Drawing.Colors.BlueViolet, 2)

    def OnPaint(self, e):
        try:
            y_offset = -self.scroll_offset

            y = y_offset
            for item in self.content:
                e.Graphics.DrawText(Eto.Drawing.Font("Arial", 12), Eto.Drawing.Colors.Black, 10, y, item)
                y += 25  # Adjust as needed for line spacing

            # Draw the scroll handle
            handle_height_ratio = self.Height / self.content_height
            handle_height = self.Height * handle_height_ratio
            handle_width = 10
            handle_position = (self.scroll_offset / self.content_height) * self.Height
            handle_rect = Eto.Drawing.Rectangle(self.Width - handle_width, int(handle_position), handle_width, int(handle_height))  # Adjust for handle size and position
            handle_round_rect = Eto.Drawing.GraphicsPath.GetRoundRect(handle_rect, (handle_width / 2))
            e.Graphics.FillPath(Eto.Drawing.Colors.BlueViolet, handle_round_rect)
            e.Graphics.DrawLine(self.scroll_pen, Eto.Drawing.PointF(self.Width - (handle_width / 2), 5), Eto.Drawing.PointF(self.Width - (handle_width / 2), self.Height - 5))

        except Exception as ex:
            print(ex)

    def OnMouseDown(self, e):
        try:
            self.mouse_down = True
            self.start_mouse_y = e.Location.Y
            self.start_scroll_offset = self.scroll_offset

        except Exception as ex:
            print(ex)

    def OnMouseUp(self, e):
        try:
            self.mouse_down = False

        except Exception as ex:
            print(ex)

    def OnMouseMove(self, e):
        try:
            if e.Buttons == Eto.Forms.MouseButtons.Primary and self.mouse_down:
                delta = e.Location.Y - self.start_mouse_y
                # self.scroll_offset = self.start_scroll_offset - delta  # Use this for inverted grab scrolling
                self.scroll_offset = self.start_scroll_offset + delta
                self.scroll_offset = max(0, min(self.scroll_offset, self.content_height - self.Height))
                self.Invalidate()

        except Exception as ex:
            print(ex)

    def OnMouseWheel(self, e):
        try:
            self.scroll_offset -= e.Delta.Height * self.scroll_speed
            self.scroll_offset = max(0, min(self.scroll_offset, self.content_height - self.Height))
            self.Invalidate()

        except Exception as ex:
            print(ex)


class MainForm(Eto.Forms.Form):
    def __init__(self):
        super().__init__()

        # Set Form General Settings
        self.Title = "Main Form"
        self.Size = Eto.Drawing.Size(300, 300)
        self.Resizable = False
        self.MovableByWindowBackground = False

        self.CreateUI()  # Call Initially To Create UI

    def CreateUI(self):
        # Create Pixel Layout For Background Graphics
        self.layout = Eto.Forms.PixelLayout()

        # Setup Bitmap For Background Graphics To Be Drawn On
        pixelformat = Eto.Drawing.PixelFormat.Format32bppRgba
        bitmap = Eto.Drawing.Bitmap(self.Size, pixelformat)
        self.graphics = Eto.Drawing.Graphics(bitmap)

        # Create a stack layout to hold the search bar and search results
        self.stack_layout = Eto.Forms.StackLayout()
        self.stack_layout.Orientation = Eto.Forms.Orientation.Vertical

        test_header = Eto.Forms.Label()
        test_header.Text = "Test Scrollable Header"

        example_scrollable_content = [
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small"
        ]

        # Create the custom scrollable area
        self.custom_scrollable = CustomScrollable()
        self.custom_scrollable.Width = self.Width - 40
        self.custom_scrollable.Height = self.Height - 100
        self.custom_scrollable.BackgroundColor = Eto.Drawing.Colors.Transparent
        self.custom_scrollable.set_content(example_scrollable_content)

        # Add the title header bar to the stack layout
        self.stack_layout.Items.Add(Eto.Forms.StackLayoutItem(test_header))
        self.stack_layout.Items.Add(Eto.Forms.StackLayoutItem(self.custom_scrollable))

        # Add the stack layout to the pixel layout and set its bounds
        self.layout.Add(self.stack_layout, Eto.Drawing.Point(5, 5))

        # Set the content of the form to be the pixel layout
        self.Content = self.layout


def EstablishForm():
    main_form = MainForm()
    main_form.Owner = Rhino.UI.RhinoEtoApp.MainWindow
    main_form.Show()


if __name__ == "__main__":
    EstablishForm()

And what it looks like visually:
image

Does anyone have any ideas why the MouseEventArgs are crashing?

Thank you all for your help!

2 Likes

Here’s the the script using just graphics.DrawText which visually “operates” how I would like it to but doesn’t have the layout logic integrated:

import Rhino
import Eto


class CustomScrollable(Eto.Forms.Drawable):
    def __init__(self, control):
        super(CustomScrollable, self).__init__()
        self._control = control
        self.AutoScroll = True  # Enable automatic scrolling
        self.scrollFactor = 25

    def OnMouseWheel(self, e):
        moveDelta = e.Delta.Height * self.scrollFactor
        # Rhino.RhinoApp.WriteLine(moveDelta)
        self._currentLocation += moveDelta
        self.CheckScrollBarExceedsBounds(self._currentLocation)
        self._layout.Move(self._control, Eto.Drawing.Point(0, self._currentLocation))


class MainForm(Eto.Forms.Form):
    def __init__(self):
        super().__init__()

        global main_form_instance
        main_form_instance = self

        # Set Form General Settings
        self.Title = "Main Form"
        self.Size = Eto.Drawing.Size(300, 300)
        self.Resizable = False
        self.MovableByWindowBackground = False

        self.CreateUI()  # Call Initially To Create UI

    def CreateUI(self):
        # Create Pixel Layout For Background Graphics
        self.layout = Eto.Forms.PixelLayout()

        # Setup Bitmap For Background Graphics To Be Drawn On
        pixelformat = Eto.Drawing.PixelFormat.Format32bppRgba
        bitmap = Eto.Drawing.Bitmap(self.Size, pixelformat)
        self.graphics = Eto.Drawing.Graphics(bitmap)

        # Create a stack layout to hold the search bar and search results
        self.stack_layout = Eto.Forms.StackLayout()
        self.stack_layout.Orientation = Eto.Forms.Orientation.Vertical

        test_header = Eto.Forms.Label()
        test_header.Text = "Test Scrollable Header"

        example_scrollable_content = [
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small"
        ]

        self.dynamic_layout = Eto.Forms.DynamicLayout()

        for content in example_scrollable_content:
            button = Eto.Forms.Button()
            button.Text = content
            self.dynamic_layout.AddRow(button)

        # Create the custom scrollable area
        self.custom_scrollable = CustomScrollable(self.dynamic_layout)
        # self.custom_scrollable = Eto.Forms.Scrollable()
        self.custom_scrollable.Width = self.Width - 40
        self.custom_scrollable.Height = self.Height - 100
        self.custom_scrollable.BackgroundColor = Eto.Drawing.Colors.Transparent
        self.custom_scrollable.Content = self.dynamic_layout

        self.stack_layout.Items.Add(Eto.Forms.StackLayoutItem(self.custom_scrollable))

        # Add the stack layout to the pixel layout and set its bounds
        self.layout.Add(self.stack_layout, Eto.Drawing.Point(5, 5))

        # Set the content of the form to be the pixel layout
        self.Content = self.layout


def EstablishForm():
    main_form = MainForm()
    main_form.Owner = Rhino.UI.RhinoEtoApp.MainWindow
    main_form.Show()


if __name__ == "__main__":
    EstablishForm()

And here’s what it looks like, you can see the buttons show up when passed as an argument for the CustomScrollable class but I can no longer see the Custom Scrollbar:

image

I can’t for the life of me figure out how to get the layout of controls to show up in the custom drawable. It seems to be one or the other right now. I either can see the scrollbar but not the buttons, or I see the buttons but now the scroll bar:

Here’s that effort:

import Rhino
import Eto

main_form_instance = None


class CustomScrollable(Eto.Forms.Drawable):
    def __init__(self):
        super(CustomScrollable, self).__init__()
        self.Size = Eto.Drawing.Size(300, 200)
        self.content_height = 0
        self.scroll_offset = 0
        self.mouse_down = False
        self.start_mouse_y = 0
        self.start_scroll_offset = 0
        self.content = []
        self.scroll_speed = 25  # Scroll speed factor (same as item height)

        self.scroll_pen = Eto.Drawing.Pen(Eto.Drawing.Colors.BlueViolet, 2)

    def set_content(self, content):
        self.content = content
        self.content_height = 25  # Adjust as needed for item height
        self.Invalidate()

    def OnPaint(self, e):
        try:
            y_offset = -self.scroll_offset

            y = y_offset
            for control in self.content:
                y += 25  # Adjust as needed for content snapping

            # Draw the scroll handle
            handle_height_ratio = self.Height / self.content_height
            handle_height = self.Height * handle_height_ratio
            handle_width = 10
            handle_position = (self.scroll_offset / self.content_height) * self.Height
            handle_rect = Eto.Drawing.Rectangle(self.Width - handle_width, int(handle_position), handle_width, int(handle_height))  # Adjust for handle size and position
            handle_round_rect = Eto.Drawing.GraphicsPath.GetRoundRect(handle_rect, (handle_width / 2))
            e.Graphics.FillPath(Eto.Drawing.Colors.BlueViolet, handle_round_rect)
            e.Graphics.DrawLine(self.scroll_pen, Eto.Drawing.PointF(self.Width - (handle_width / 2), 5), Eto.Drawing.PointF(self.Width - (handle_width / 2), self.Height - 5))

        except Exception as ex:
            print(f"OnPaint Exception: {ex}")

    def OnMouseDown(self, e):
        try:
            if main_form_instance.HasFocus:
                # self.mouse_down = True
                pass
                # self.start_mouse_y = e.Location.Y
                # self.start_scroll_offset = self.scroll_offset

        except Exception as ex:
            print(f"OnMouseDown Exception: {ex}")

    def OnMouseUp(self, e):
        try:
            self.mouse_down = False
            if main_form_instance.HasFocus:
                self.start_mouse_y = e.Location.Y
                self.start_scroll_offset = self.scroll_offset
                # Calculate the click position as a percentage of the scrollable height
                click_position_ratio = e.Location.Y / self.Height
                self.scroll_offset = click_position_ratio * self.content_height
                self.scroll_offset = max(0, min(self.scroll_offset, self.content_height - self.Height))
                # Snap to the nearest item
                self.scroll_offset = round(self.scroll_offset / 25) * 25
                self.Invalidate()

        except Exception as ex:
            print(f"OnMouseUp Exception: {ex}")

    # def OnMouseMove(self, e):
    #     try:
    #         if main_form_instance.HasFocus and e.Buttons == Eto.Forms.MouseButtons.Primary and self.mouse_down:
    #             delta = e.Location.Y - self.start_mouse_y
    #             self.scroll_offset = self.start_scroll_offset + delta
    #             self.scroll_offset = max(0, min(self.scroll_offset, self.content_height - self.Height))
    #             self.Invalidate()

    #     except Exception as ex:
    #         print(f"OnMouseMove Exception: {ex}")

    def OnMouseWheel(self, e):
        try:
            if e.Buttons is not None:
                self.scroll_offset -= e.Delta.Height * self.scroll_speed
                self.scroll_offset = max(0, min(self.scroll_offset, self.content_height - self.Height))
                # Snap to the nearest item
                self.scroll_offset = round(self.scroll_offset / 25) * 25
                self.Invalidate()

        except Exception as ex:
            print(f"OnMouseWheel Exception: {ex}")


class MainForm(Eto.Forms.Form):
    def __init__(self):
        super().__init__()

        global main_form_instance
        main_form_instance = self

        # Set Form General Settings
        self.Title = "Main Form"
        self.Size = Eto.Drawing.Size(300, 300)
        self.Resizable = False
        self.MovableByWindowBackground = False

        self.CreateUI()  # Call Initially To Create UI

    def CreateUI(self):
        # Create Pixel Layout For Background Graphics
        self.layout = Eto.Forms.PixelLayout()

        # Setup Bitmap For Background Graphics To Be Drawn On
        pixelformat = Eto.Drawing.PixelFormat.Format32bppRgba
        bitmap = Eto.Drawing.Bitmap(self.Size, pixelformat)
        self.graphics = Eto.Drawing.Graphics(bitmap)

        # Create a stack layout to hold the search bar and search results
        self.stack_layout = Eto.Forms.StackLayout()
        self.stack_layout.Orientation = Eto.Forms.Orientation.Vertical

        test_header = Eto.Forms.Label()
        test_header.Text = "Test Scrollable Header"

        example_scrollable_content = [
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small"
        ]

        self.dynamic_layout = Eto.Forms.DynamicLayout()

        for content in example_scrollable_content:
            button = Eto.Forms.Button()
            button.Text = content
            self.dynamic_layout.AddRow(button)

        self.custom_scrollable = CustomScrollable()
        self.custom_scrollable.Width = self.Width - 40
        self.custom_scrollable.Height = self.Height - 100
        self.custom_scrollable.BackgroundColor = Eto.Drawing.Colors.Transparent
        self.custom_scrollable.set_content( self.dynamic_layout)

        # Add the title header bar to the stack layout
        self.stack_layout.Items.Add(Eto.Forms.StackLayoutItem(test_header))
        self.stack_layout.Items.Add(Eto.Forms.StackLayoutItem(self.custom_scrollable))

        # Add the stack layout to the pixel layout and set its bounds
        self.layout.Add(self.stack_layout, Eto.Drawing.Point(5, 5))

        # Set the content of the form to be the pixel layout
        self.Content = self.layout


def EstablishForm():
    main_form = MainForm()
    main_form.Owner = Rhino.UI.RhinoEtoApp.MainWindow
    main_form.Show()


if __name__ == "__main__":
    EstablishForm()

I appreciate any leads! I tried to “pythonize” this C# implementation of a similar problem but could not get it working… Here’s that effort in code:

import Rhino
import Eto


class CustomScrollable(Eto.Forms.Drawable):
    def __init__(self, control):
        super(CustomScrollable, self).__init__()
        self._control = control
        self.AutoScroll = True  # Enable automatic scrolling
        self.scrollFactor = 25

    def OnMouseWheel(self, e):
        moveDelta = e.Delta.Height * self.scrollFactor
        # Rhino.RhinoApp.WriteLine(moveDelta)
        self._currentLocation += moveDelta
        self.CheckScrollBarExceedsBounds(self._currentLocation)
        self._layout.Move(self._control, Eto.Drawing.Point(0, self._currentLocation))


class MainForm(Eto.Forms.Form):
    def __init__(self):
        super().__init__()

        global main_form_instance
        main_form_instance = self

        # Set Form General Settings
        self.Title = "Main Form"
        self.Size = Eto.Drawing.Size(300, 300)
        self.Resizable = False
        self.MovableByWindowBackground = False

        self.CreateUI()  # Call Initially To Create UI

    def CreateUI(self):
        # Create Pixel Layout For Background Graphics
        self.layout = Eto.Forms.PixelLayout()

        # Setup Bitmap For Background Graphics To Be Drawn On
        pixelformat = Eto.Drawing.PixelFormat.Format32bppRgba
        bitmap = Eto.Drawing.Bitmap(self.Size, pixelformat)
        self.graphics = Eto.Drawing.Graphics(bitmap)

        # Create a stack layout to hold the search bar and search results
        self.stack_layout = Eto.Forms.StackLayout()
        self.stack_layout.Orientation = Eto.Forms.Orientation.Vertical

        test_header = Eto.Forms.Label()
        test_header.Text = "Test Scrollable Header"

        example_scrollable_content = [
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small",
            "Something Here", "Something There", "Something Else",
            "Something Over", "Something Under", "Something Near",
            "Something Far", "Something Large", "Something Small"
        ]

        self.dynamic_layout = Eto.Forms.DynamicLayout()

        for content in example_scrollable_content:
            button = Eto.Forms.Button()
            button.Text = content
            self.dynamic_layout.AddRow(button)

        # Create the custom scrollable area
        self.custom_scrollable = CustomScrollable(self.dynamic_layout)
        # self.custom_scrollable = Eto.Forms.Scrollable()
        self.custom_scrollable.Width = self.Width - 40
        self.custom_scrollable.Height = self.Height - 100
        self.custom_scrollable.BackgroundColor = Eto.Drawing.Colors.Transparent
        self.custom_scrollable.Content = self.dynamic_layout

        self.stack_layout.Items.Add(Eto.Forms.StackLayoutItem(self.custom_scrollable))

        # Add the stack layout to the pixel layout and set its bounds
        self.layout.Add(self.stack_layout, Eto.Drawing.Point(5, 5))

        # Set the content of the form to be the pixel layout
        self.Content = self.layout


def EstablishForm():
    main_form = MainForm()
    main_form.Owner = Rhino.UI.RhinoEtoApp.MainWindow
    main_form.Show()


if __name__ == "__main__":
    EstablishForm()

Thank you all, I’m stuck on how to achieve a drawable scrollbar in python that accepts a layout as its content. I appreciate the help!

1 Like