Scrollbar problems on Windows

In my GUI application, I have a number of scrollbar-related problems in Windows that I don't see on Linux. My app has a custom canvas% with manual scrollbars.

The main problem occurs when scrolling a very large virtual canvas (> 30,000 pixels high or so). Scrolling via dragging the scroll thumb(?) seems to work as expected until I release the left mouse button. At that point I see an on-scroll event with a bogus position in the event argument. It kinda seems like an integer overflow issue at around 32,768 but it isn't super consistent. Scrolling via set-scroll-pos works fine even with very large values.

I can easily have a virtual canvas with dimensions > 500,000 pixels, so this is a problem.

On a more general note, I've implemented scrolling using units of pixels. Is that how manual scrollbars were intended to be used? I suppose I could use a larger unit, but with the sizes I deal with I expect I will still run into this issue if I keep any degree of fine-grained scrolling.

Note: it looks like the 32,000 limit is a known issue. See this commit:
https://github.com/racket/gui/commit/4ad479277821641fb39b8ae8e9c20a76b36e9e9a

Also, see an issue I previously opened concerning the limit to set-scroll-range:
https://github.com/racket/gui/issues/295

Any thoughts or suggestions are appreciated!

Right now I'm leaning toward increasing the "scroll unit" when the scroll range approaches 32,000. That way I can retain fine grained scrolling for smaller canvases.

Hopefully that doesn't lead to any of the other problems I've seen on Windows. Namely that I've had trouble when dynamically changing set-scroll-page. It sometimes causes the scrollbar to appear (albeit in an inactive state) even though it should be hidden by show-scrollbars. Similar things would also happen when setting the scroll range to 0 on Windows.

Here's what I did to avoid the scroll range limit:

(define hscroll-enabled? #f)
(define vscroll-enabled? #f)

(define/override (get-scroll-pos which)
  (cond
    [(eq? which 'vertical)
     (* (super get-scroll-pos which) scrollbar-vert-step-size)]
    [else
     (super get-scroll-pos which)]))
 
(define/override (get-scroll-range which)
  (cond
    [(eq? which 'vertical)
     (* (super get-scroll-range which) scrollbar-vert-step-size)]
    [else
     (super get-scroll-range which)]))
 
(define/override (set-scroll-pos which value)
  (cond
    [(eq? which 'vertical)
     ;(printf " set-scroll-pos: ~a, ~a(~a)~n" which value (quotient value scrollbar-vert-step-size))
     (super set-scroll-pos which (quotient value scrollbar-vert-step-size))]
    [else
     (super set-scroll-pos which value)]))

(define/override (set-scroll-range which value)
  (cond
    [(eq? which 'vertical)
     ;(printf " set-scroll-range: ~a, ~a(~a)~n" which value (/ value scrollbar-vert-step-size))
     (super set-scroll-range which (quotient value scrollbar-vert-step-size))]
    [else
     (super set-scroll-range which value)]))

(define/override (set-scroll-page which value)
  (cond
    [(eq? which 'vertical)
     ;(printf " set-scroll-page: ~a, ~a(~a)~n" which value (/ value scrollbar-vert-step-size))
     (super set-scroll-page which (quotient value scrollbar-vert-step-size))]
    [else
     (super set-scroll-page which value)]))

;; update the manual scrollbars range and hide/unhide them
;; new width and height are in pixels
(define (update-scrollbars new-width new-height)
  #;(printf "update-scrollbars, thread=~a~n" (current-thread))
  (when (semaphore-try-wait? edit-lock)
    (define-values (dw dh) (get-drawable-size))
    (set-scroll-range 'horizontal (exact-truncate (max 1 (- new-width dw))))
    (set-scroll-range 'vertical (exact-truncate (max 1 (- new-height dh))))
    (set-scroll-page 'horizontal 100)
    (set-scroll-page 'vertical (max 1 dh))
    ;; when scrollbar's are enabled, we need to recreate the offscreen bitmap because the client size changes
    ;(printf "hscroll-enabled=~a, vscroll-enabled=~a~n" hscroll-enabled? vscroll-enabled?)
    (when (not (equal? hscroll-enabled? (> new-width dw)))
      (set! hscroll-enabled? (> new-width dw))
      ;(printf " hscroll enabled changed to ~a (nw=~a, dw=~a)~n" hscroll-enabled? new-width dw)
      (send offscreen-dc set-bitmap #f)
      (show-scrollbars hscroll-enabled? vscroll-enabled?))
    (when (not (equal? vscroll-enabled? (> new-height dh)))
      (set! vscroll-enabled? (> new-height dh))
      ;(printf " vscroll enabled changed to ~a (nh=~a, dh=~a)~n" vscroll-enabled? new-height dh)
      (send offscreen-dc set-bitmap #f)
      (show-scrollbars hscroll-enabled? vscroll-enabled?)
      ;; the vertial scrollbar changes the client width and thus the layout
      ;; so if it changes we need to rerun the layout
      (reset-layout)
      (define-values (vx vy) (get-virtual-size))
      (define-values (ndw ndh) (get-drawable-size))
      (set-scroll-range 'horizontal (exact-truncate (max 1 (- vx ndw))))
      (set-scroll-range 'vertical (exact-truncate (max 1 (- vy ndh))))
      (set-scroll-page 'vertical (max 1 ndh)))
    (semaphore-post edit-lock)))

Elsewhere in the class, I set scrollbar-vert-step-size after loading the canvas's contents:

(if (< canvas-height 30000)
      (set! scrollbar-vert-step-size 1)
      (set! scrollbar-vert-step-size (ceiling (/ canvas-height 30000))))

This appears to work well. However, there are still issues(Windows only) with hidden scrollbars appearing, presumably due to calls to set-croll-page or set-scroll-range in my update-scrollbars function.

1 Like

Don't know about Linux, but Windows scrollbars do have a (small-ish) limit to their range. Best way to deal with it is to pick a useful scale, e.g., 100, 1000, etc., limit the scrollbar to that range, and then map it to your scrollable area.

Yes, that is what I finally settled on. I dynamically calculate the range so that I use as fine-grained a scrollbar step size as I can.

This is still useful on Linux in cases where the scrollbar range would exceed 1 million without any scaling. I haven't tried it in MacOS yet.