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:
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:
Does anyone have any ideas why the MouseEventArgs are crashing?
Thank you all for your help!