The background: Most of us know the SysListView32 common control and the equivalent wrapper ListView class provided by the .NET Framework. A little depth into its internals show that the scroll bars it provides for scrolling its contents are NOT controls themselves, but are managed by the SysListView32 control.
The goal: Always draw scroll bars even if it has no ListViewItems to display or has very few such that no scroll bars are needed anyway; sort of like mimicking the RichTextBox class with its ScrollBars property set to ForcedBoth. Or kinda like this ListBox:
The problem(s):
.NET has NO sugar at all for scroll bars within a ListView.
Win32 documentation does not state when to show/hide and/or enable/disable scrollbars.
My workaround(s):
override the WndProc in a derived class and handle its WM_HSCROLL and WM_VSCROLL messages as per steps 2 and 3.
Call base.WndProc to do the actually required processing of the scroll functionality.
Create a method like WmScroll and do my processing on it immediately after base.WndProc has returned.
This consists of a p/invoke call to GetScrollInfo. Determine if a scroll bar is actually needed. If it's not then call ShowScrollBar and EnableScrollBar with required values to draw visibly disabled scroll bars.
Problems with the workaround:
It barely works. The scroll bars are displayed and disabled but are like the ones under Windows Classic Theme.
It hides the collapse buttons of each ListViewGroup, rendering them useless!
The descriptive image:
The long awaited actual question:
How do I force scroll bars to always be Visible within a ListView irrespective of the number of ListViewItems and disable them if they are unnecessary, at the same time avoiding size miscalculation (to display collapse buttons of the ListViewGroups) and theme deterioration?
Answers without code, and answers with code in C#, VB.NET and C++/CLR are welcome. If you post code in any other language supported by .NET, please also leave a link to a code conversion website I may use if the code seems, uh, incomprehensible.
Information:
Firstly, I have to admit this is an okay answer and not the best/most efficient one. If you have a different answer from mine, please post it.
Secondly, this answer owes some credit to Plutonix's answer, experimenting with which I learned that by default ListView does not have WS_HSCROLL | WS_VSCROLL flags set in its styles.
This is why my previous workaround had problem with themes.
These Classic scroll bars are ones Windows provides to Controls that do not have these flags set.
Changing the CreateParams does not work either. You have to set it manually in the OnHandleCreated method using SetWindowLong.
The solution I am posting does not use the above technique. Apparently, calling ShowScrollBar for each window message forces these flags to be set.
The Solution:
Define your WndProc like the following:
protected override void WndPoc(ref Message m)
{
//custom code before calling base.WndProc
base.WndProc(ref m);
//custom after base.WndProc returns
WmScroll(); //VERY INEFFICIENT, called for each message :(
}
Define WmScroll() as follows:
protected virtual void WmScroll()
{
NativeMethods.ShowScrollBar(Handle, SB_BOTH, true);
//si.fMask = SIF_PAGE | SIF_RANGE <- initialized in .ctor
NativeMethods.GetScrollInfo(Handle, SB_HORZ, ref si);
if(si.nMax < si.nPage)
NativeMethods.EnableScrollBar(Handle, SB_HORZ, ESB_DISABLE_BOTH);
else
NativeMethods.EnableScrollBar(Handle, SB_HORZ, ESB_ENABLE_BOTH);
NativeMethods.GetScrollInfo(Handle, SB_VERT, ref si);
if(si.nMax < si.nPage)
NativeMethods.EnableScrollBar(Handle, SB_VERT, ESB_DISABLE_BOTH);
else
NativeMethods.EnableScrollBar(Handle, SB_VERT, ESB_ENABLE_BOTH);
}
Output:
It now, looks like:
These are with another item added featuring the horizontal scroll and working ListViewGroup collapse button:
Imperfection, yes there is:
A call to AutoResizeColumns is required if group collapse changes effective text width, otherwise the vertical scroll bar hides the collapse buttons.
Related
I am writing an application in .NET that has a plugin interface. That plugin interface provides several ways to draw information (controls) onto the surface of the application window. While there are several reasons why I am doing this, the main reason is to provide custom colorization to text, either through the use of a graphic or directly manipulating the color of the text based on the background color. I do this through the use of a "text mask" which is a black and white bitmap that works as an "alpha" map to let the Paint method know where to apply the texture/color changes.
The plugin developer has the option of using regular text (such as with a label), mask text (which is drawn to the mask rather than as a regular control), OR letting the user decide. To go along with this, I have provided a modified label class that can either be drawn "normally' (when the text mask is not set for the control), or to the text mask when the User OR Developer decides (depending on what the plugin developer wishes to offer to the user). Here is the class's code so that you understand how this is being done:
public class MaskingLabel : Label
{
private static readonly SolidBrush maskBrush = new SolidBrush(Color.White);
public Bitmap Mask { get; set; }
public MaskingLabel() : base() { }
protected override void OnPaint(PaintEventArgs e)
{
if (Mask == null)
base.OnPaint(e);
else
{
Graphics g = Graphics.FromImage(Mask);
g.DrawString(Text, Font, maskBrush, Location);
}
}
}
The problem I am running into is that this approach requires that I handle controls in a very specific order so that the form is drawn correctly. I need to find the most efficient approach to get the tasks listed below done in the order given. I have thought of three possibilities discussed further down. For reference, this is the order in which tasks must be done:
All "MaskingLabel" controls that have the bitmap object set to the mask must be drawn first so that the mask is created before the next step.
The mask is applied to the background picture.
The resulting Bitmap is drawn in a way similar to the way a background would be drawn (except that it is modified first).
The rest of the controls are drawn as normal.
Is there a way for me to insure this happens without separating the controls manually? My first guess is no. As such, I have a few guesses below about how I should go about this. I was hoping someone with more in depth knowledge of GDI+ could offer some insight.
One idea that has occurred to me is to draw the masked controls during the OnPaintBackground method. However, I don't want to waste time by painting the controls twice. This means I would need to filter out which controls are drawn during the main Paint method which effectively leads us to option 2 (FAIK):
I can manually filter out the controls which draw to the mask so that they don't get added to the control. My question here though is would they get drawn at all? Can I manually force them to invoke the OnPaint method?
If doing that wouldn't work, then perhaps I can create a separate derived panel control to serve as a "backdrop" child control that acts as the background picture which can be forced to be drawn first?
EDIT (With Part of the answer):
I realized after posting this that I already have part of the solution built into my project. Still, I think it is a legitimate question to ask, so if anyone can add insight beyond what I have done in my description below, it is welcome.
Specifically, my project has only two controls that are added to the "root" form: a bar that goes to the top (docked at the top when it is shown), and a transparent panel that occupies the rest of the space (with a dock style set to fill). So my solution would be to add the mask controls to the main form and add all the rest to the panel. This only leaves one remaining issue to be resolved: How do I make sure that the panel and the bar are drawn last? (As part of step 4 in the first list?)
Short version:
I'm using a WinForms control which flickers when being redrawn often. None of the existing "Double-buffer enable"-type solutions I found are effective, since this control literally does two distinct paint operations directly to it's drawing surface when redrawing. This control needs to be interactive (hence topmost on the form). Given that I can execute instructions right before this control starts repainting and right after it finishes repainting, is there anything the parent container or the containing Form can do to eliminate this control's flicker?
Full explanation:
I'm making a user control. A ListView as one of the children on it. I'm using the ListView to have columns on my control and all the functionality that a ListView's columns provide (hence, only the column header part of the ListView is actually visible on my control). I need to draw some additional information (let's call it "the overlay") where the ListView's column header are, but don't want to alter the 'look' of the column header otherwise. I obviously can't just draw the overlay from my user control in any way (the ListView is a child on my control and cover's up my control's drawing surface). I can't have a control on top of the ListView to function as the overlay's drawing surface - sure I can paint the ListView just fine on it and even preserve interactivity by messing with WM_NCHITTEST messages, but now the ListView does not get repainted, and I'm stuck with either redrawing the entire ListView on any mouse events or manually keeping track of which regions of the ListView need to be redrawn based on the events my overlay control receives (way too troublesome).
So the obvious solution is to override the ListView's column header's WndProc (like ObjectListView does it) and add my overlay drawing code there. So I have:
protected override void WndProc(ref Message m) {
switch (m.Msg) {
case WM_PAINT:
RECT r = new RECT();
WinAPI.GetUpdateRect(Handle, ref r, false); //user32.dll - bool GetUpdateRect(IntPtr hWnd, ref RECT rect, bool erase)
Rectangle rc = new Rectangle(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
/* marker 1 */
base.WndProc(ref m); //draw the column headers
/* marker 2 */
MyPaint(rc, Graphics.FromHwnd(Handle)); //draw overlay (I'm aware of graphics needing to be disposed)
/* marker 3 */
return;
}
base.WndProc(ref m);
}
Looks great! But when redrawing often, sometimes I will see on the screen the result of drawing only up to marker 2. It makes sense, of course, since I now have two distinct draw operations right onto the ListView's column header's drawing surface. Still, I can set whatever state I want in marker 1 and marker 3 so there must be some way for me to be able to suspend some drawing somewhere somehow right? Well, I can't for the life of me think of a way to do that. So there's my question - is it possible?
Basically, whatever solutions I tried to somehow suspend redraw, obviously trigger a redraw on 'un-suspend' resulting in endless WM_PAINTs. I was really trying to avoid just getting rid of base.WndProc(ref m) and drawing the column headers myself, along with whatever style drawing stuff that has to come along with it to preserve the column headers' native 'look'. But after fiddling with WndProcs for a few days, I'm starting to think that drawing the column headers from scratch is actually the least painful solution...
Well, I just realized that all this time I missed one rather simple solution - just have the base ListView draw to a different surface! So now I have:
case WM_Paint:
PAINTSTRUCT ps;
WinAPI.BeginPaint(Handle, out ps); //user32 native calls
Rectangle rc = new Rectangle(ps.rcPaint.Left, ps.rcPaint.Top, ps.rcPaint.Right - ps.rcPaint.Left, ps.rcPaint.Bottom - ps.rcPaint.Top);
listview.DrawToBitmap(bmp, listview.ClientRectangle); //send yourself a WM_PRINT, pretty much
buf.Graphics.SetClip(rc); //now just draw everything to a BufferedGraphicsContext buffer
buf.Graphics.DrawImageUnscaled(bmp, 0, 0); //draw base ListView column header
MyPaint(rc, buf.Graphics); //draw overlay
buf.Render(Graphics.FromHwnd(Handle)); //flush buffer
WinAPI.EndPaint(Handle, ref ps); //validate drawing rectangle
return;
No more flicker! Best part is, this answers the original question in a generic sense, as this technique can be used to 'fix' any badly-drawing control (or add additional drawing code to one), so long as the control properly handles WM_PRINT messages.
The background: Most of us know the SysListView32 common control and the equivalent wrapper ListView class provided by the .NET Framework. A little depth into its internals show that the scroll bars it provides for scrolling its contents are NOT controls themselves, but are managed by the SysListView32 control.
The goal: Always draw scroll bars even if it has no ListViewItems to display or has very few such that no scroll bars are needed anyway; sort of like mimicking the RichTextBox class with its ScrollBars property set to ForcedBoth. Or kinda like this ListBox:
The problem(s):
.NET has NO sugar at all for scroll bars within a ListView.
Win32 documentation does not state when to show/hide and/or enable/disable scrollbars.
My workaround(s):
override the WndProc in a derived class and handle its WM_HSCROLL and WM_VSCROLL messages as per steps 2 and 3.
Call base.WndProc to do the actually required processing of the scroll functionality.
Create a method like WmScroll and do my processing on it immediately after base.WndProc has returned.
This consists of a p/invoke call to GetScrollInfo. Determine if a scroll bar is actually needed. If it's not then call ShowScrollBar and EnableScrollBar with required values to draw visibly disabled scroll bars.
Problems with the workaround:
It barely works. The scroll bars are displayed and disabled but are like the ones under Windows Classic Theme.
It hides the collapse buttons of each ListViewGroup, rendering them useless!
The descriptive image:
The long awaited actual question:
How do I force scroll bars to always be Visible within a ListView irrespective of the number of ListViewItems and disable them if they are unnecessary, at the same time avoiding size miscalculation (to display collapse buttons of the ListViewGroups) and theme deterioration?
Answers without code, and answers with code in C#, VB.NET and C++/CLR are welcome. If you post code in any other language supported by .NET, please also leave a link to a code conversion website I may use if the code seems, uh, incomprehensible.
Information:
Firstly, I have to admit this is an okay answer and not the best/most efficient one. If you have a different answer from mine, please post it.
Secondly, this answer owes some credit to Plutonix's answer, experimenting with which I learned that by default ListView does not have WS_HSCROLL | WS_VSCROLL flags set in its styles.
This is why my previous workaround had problem with themes.
These Classic scroll bars are ones Windows provides to Controls that do not have these flags set.
Changing the CreateParams does not work either. You have to set it manually in the OnHandleCreated method using SetWindowLong.
The solution I am posting does not use the above technique. Apparently, calling ShowScrollBar for each window message forces these flags to be set.
The Solution:
Define your WndProc like the following:
protected override void WndPoc(ref Message m)
{
//custom code before calling base.WndProc
base.WndProc(ref m);
//custom after base.WndProc returns
WmScroll(); //VERY INEFFICIENT, called for each message :(
}
Define WmScroll() as follows:
protected virtual void WmScroll()
{
NativeMethods.ShowScrollBar(Handle, SB_BOTH, true);
//si.fMask = SIF_PAGE | SIF_RANGE <- initialized in .ctor
NativeMethods.GetScrollInfo(Handle, SB_HORZ, ref si);
if(si.nMax < si.nPage)
NativeMethods.EnableScrollBar(Handle, SB_HORZ, ESB_DISABLE_BOTH);
else
NativeMethods.EnableScrollBar(Handle, SB_HORZ, ESB_ENABLE_BOTH);
NativeMethods.GetScrollInfo(Handle, SB_VERT, ref si);
if(si.nMax < si.nPage)
NativeMethods.EnableScrollBar(Handle, SB_VERT, ESB_DISABLE_BOTH);
else
NativeMethods.EnableScrollBar(Handle, SB_VERT, ESB_ENABLE_BOTH);
}
Output:
It now, looks like:
These are with another item added featuring the horizontal scroll and working ListViewGroup collapse button:
Imperfection, yes there is:
A call to AutoResizeColumns is required if group collapse changes effective text width, otherwise the vertical scroll bar hides the collapse buttons.
I have a window that has a menu, a toolbar at the top, and various other controls. I then have my own control that derives from ContentControl that I want to have use up all remaining space. I can't leave it to its own devices unfortunately, because the control is a Win32 control that's sort of... put inside this WPF control, and I need to use SetWindowPos.
At the moment what I am doing is using ArrangeOverride, getting the MainWindow.Content control and looking at the Height and Width. I then use Size I get in as a parameter and call the SetWindowPos function. It's written in C++/CLI, and here's the code:
Size WebView::ArrangeOverride(Size finalSize)
{
Application::Current->MainWindow->Measure(finalSize);
UIElement^ obj = dynamic_cast<UIElement^>(Application::Current->MainWindow->Content);
double objHei = obj->RenderSize.Height;
double objWid = obj->RenderSize.Width;
SetWindowPos(hWnd, NULL, objWid-finalSize.Width, objHei-finalSize.Height, finalSize.Width, finalSize.Height, NULL);
So in my head I thought this would then set the position of the control to within the remaining available space. And it does sort of work, but it seems as if the MainWindow.Content control is not being measured until afterwards? What am I doing wrong here?
edit: most of the problems seem to be when full-screening the window and then un-fullscreening it.
I have managed to fix this by using the answer to this question here
I simply put my control into a Frame, so it'd be the parent.
Then using the Point I set the window position to that, along with the size that is passed through as a parameter to the ArrangeOverride method.
I have a Pivot which contains a WebBrowser control that practically takes up the whole page (appart from the Pivot header of course).
I would like to figure out how to make the WebBrowser control allow for the user to swipe left/right to activate the Pivot control. Currently it just pans the WebBrowser control left/right
Can this be done??
Thank
While I cannot tell you exactly how to pass the swipes to the pivot, I can tell you how to do a part of the job: how to catch/analyze/disable custom gestures over the WebBrowser.
If I remember correctly, in the 7.0:
the WebBrowser component consisted almost only of an internal TileHost wrapped in some grids/borders
the TileHost did all the work related to processing touch events
the TileHost did it completely internally (in the native layer), without the .Net seeing any manipulation-events (I think), or at least it ignored all the attempts to handle/override the manipulation-event on the upper layer. The WebBrowserInterop class was mostly empty in these matters.
Now, in the 7.5 that I have (maybe on 7.1 too, I dont know), it seems that the MS is working really hard on some WebBrowser manipulation problems --- I think they are working towards having the scrolling/swiping fully processed by the .Net layer. They have written a special class PanZoomContainer and injected them into the VisualTree of WebBrowser's internal template. The WebBrowserInterop was greatly enriched with many tunnels for event notifications. The WebBrowserInterop hooks into PanZoomContainer's ManipulationEvents, then passes them to the native layer. Also, it listens to events/commands from the native layer, called for example "ZoomAndScroll" or "ShowSIP" - and mostly passes them back to the PanZoomContainer. The idea is crystal clear right? They have rewired the event handling from completely-internal to a bit of spaghetti, but have achieved passing them through the PanZoomC.
Now, whats in that for me/us?
It is the PanZoomContainer, whose Mani-Events are inspected. The TileHost does not capture them now. In this version of the WebBrowser control, it's VisualTree consists of some borders, grids, a PanZoomContainer and a TileHost (the renderer). It is similar to that:
WebBrowser
PanZoom
ContentPresenter
Border/Name="border" <- you want this one
TileHost
I've skipped a few Borders and Grids, they are mostly irrelevant to the problem. Now, if the PanZoomContainer's Mani-Events are listened to, let's block them!
Using VisualTreeHelper, just dig deeper and deeper until you find a FrameworkElement.Name=="border". This is the border that wraps the TileHost that is the "renderer" that takes 99% space of the control. Be warned that there's a ContentPresenter, so you may have to wait until the controltemplate gets instantiated (ie. Loaded/LayoutUpdated).
Once you have your hands on that Border, attach all Mani-Event handlers to it: started, delta and completed. PanZoom is a normal xaml/silverlight/.net/etc control, so it actually obeys e.Handled = true :) Now, if you want to disable ie. vertical scrolling, filter the Delta and Completed events for Translation.Y<>0. If you want to disable tapping but leave srolling/panning - filter X==0&Y==0.
And that was the easy part.
The hard part is to experiment with filtering on different Start/Delta/Stop and adjusting the behaviour to your likes.
Although it might look very nice and tempting, this will NOT get you any real/nice results easily. For example, I wrote "if you want to disable vertical scrolling, then set a filter 'if y==0 then e.handled=true' ". Great? easy? Not!
Assume we want to "disable bouncy horizontal panning" while leaving "vertical scrolling". or vice versa, whatever, it is only an example:
Here's a small rectangular device with a sensitive touchscreen. Please make such a vertical swipe/pan/drag on the screen, that the resulting X-compound will be ZERO. If you set such filter, it will be almost impossible to it properly. Your users will want to kill you for forcing them to retry-that-vertical-scrolling for five or more times, until they make a perfect vertical swipe.
Of course you can make it not ==0, but leave some small margin. ok. But if you make the margin too big, the control will catch the intermediate offaxis movement and make a tiny horizontal pan also.. After a few unlucky vertical swipes, the total horizontal pan may accumulate from those small leftovers will accumulate and the diplacement maybe will be noticeable.
But there's some another vile side effects:
Saying shortly, you have commited e.Handled=true. The event is GONE. Dead. Deased. if you just wanted the WebBrowser to SKIP for example horizontal swipes, so that the outer (Pivot) control notices them and processes..... whoops. The event is GONE. Earlier, the TileHost/PanZoomC have extinguished the events, now you have it done yourself. Sounds like a bad joke, eh?
Fortunatelly:
since you have attached your handlers to the bottommost "border", they may not only block the events, but may also actually listen&publish them elsewhere. That is, if those handlers detect an interesting movement, they may e.Handled=true on it, but at the same time they can notify your custom objects about that discovery, and ie. start your storyboards.
mani-events are at hand, but there is also a second layer that listens to the manipulations: the GestureListener/GestureService from the Silverlight Toolkit. It reports events after they are handled by mani-events, but it reports them with no regard to any e.Handled=true that were set on them. It is completely separate gesture-listening mechanism, and you can also use it to detect manipulations that were 'cancelled'
.. and so the fun goes like that and maybe even a little further.
This is similar to putting a Map inside a Pivot - which is discussed here - http://mine.tuxfamily.org/?p=111 - already mentioned in quite a few questions - https://stackoverflow.com/search?q=mine.tuxfamily.org
In general, the advice seems to be usability based:
try to avoid putting controls which use Touch inside the PivotItem's
As an aside, if you are just using the web browser control for a very small amount of static html (so you don't need any scrolling at all) then you could just remove HitTesting from the web browser.
I do not know WP7 Pivot, but are there any Preview* events on the Pivot control that allow you to handle the touches and mark them as processed?
Call the below method and pass your parameter as PivotControl x:name and WebBrowserControl x:name to this method.
Here the WebBrowserControl is placed in second pivot item i.e. Pivot Index is 1 and I am trying to swipe left or right and reach to pivot index 2 or 1 respectively.
public static void SwipteLeftRight(Microsoft.Phone.Controls.Pivot pivotControl, Microsoft.Phone.Controls.WebBrowser webBrowserControl)
{
var gesListener = GestureService.GetGestureListener(webBrowserControl);
gesListener.Flick += ((sen, args) =>
{
if (args.Direction == System.Windows.Controls.Orientation.Horizontal)
{
if (args.HorizontalVelocity < 0)
{
if (((Microsoft.Phone.Controls.PivotItem)(pivotControl.SelectedItem)).Header.ToString().Trim() == "Pivot Item name")
{
pivotControl.SelectedIndex = 2; //Next Pivot item
}
}
else if (args.HorizontalVelocity > 0)
{
if ((Microsoft.Phone.Controls.PivotItem)(pivotControl.SelectedItem)).Header.ToString().Trim() == "Pivot Item name")
{
pivotControl.SelectedIndex = 0; // Previous Pivot Item
}
}
}
});
}
It worked for me. Cheers