Why does Listbox.Drawitem get called many times? - c#

I have a databound listbox on my C# WinForm that holds strings that are links to image file locations. I want to display the image as a thumbnail for the user to click on and view. I got it working correctly by setting DrawMode=OwnerDrawVariable and handling the DrawItem and MeasureItem events.
However I noticed that I have to click exit 2 times to get out of the application (looks like it's called selectedIndexChanged on first click, then exits on second). On further inspection I noticed that the DrawItem event is fired a multitude of times when I click an item in the listbox (like 15+ times). There is only 1-2 items EVER in the listbox at a time! Why is it getting called soo many times?
I tested this with a non databound simple listbox and it does the same thing. I am only curious as I have to read the image from disk and get a thumbnail of it to put into the listbox, which if it did that 15-20 times could affect performance (and is TOTALLY unnecessary).
private void listBox1_MeasureItem(object sender, MeasureItemEventArgs e)
{
MessageBox.Show("listBox1_MeasureItem");
// Cast the sender object back to ListBox type.
ListBox theListBox = (ListBox)sender;
// Get the file path contained in each item.
DataRowView drv = (DataRowView)theListBox.Items[e.Index];
string fileString = drv.Row["fullpath"].ToString();
// Create an image object and load image file into it
Image img = Image.FromFile(fileString);
e.ItemHeight = Convert.ToInt32(img.Height * 0.15);
e.ItemWidth = Convert.ToInt32(img.Width * 0.15);
}
private void listBox1_DrawItem(object sender, DrawItemEventArgs e)
{
MessageBox.Show("listBox1_DrawItem");
// If the item is the selected item, then draw the rectangle
// filled in blue. The item is selected when a bitwise And
// of the State property and the DrawItemState.Selected
// property is true.
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
e.Graphics.FillRectangle(Brushes.CornflowerBlue, e.Bounds);
}
else
{
// Otherwise, draw the rectangle filled in beige.
e.Graphics.FillRectangle(Brushes.Beige, e.Bounds);
}
DataRowView drv = (DataRowView)listBox1.Items[e.Index];
Image img = Image.FromFile(drv.Row["fullpath"].ToString());
img = img.GetThumbnailImage(e.Bounds.Width, e.Bounds.Height, null, IntPtr.Zero);
e.Graphics.DrawImage(img, e.Bounds.X, e.Bounds.Y);
// Draw the focus rectangle around the selected item.
e.DrawFocusRectangle();
}

I am not sure and this would have to be tested, but this article makes me believe that what I am going to say is true.
Basically, if you go by the MSDN documentation:
Occurs when a visual aspect of an owner-drawn ListBox changes.
So, this means that everytime an item is added, this event is called. Also, I think even when you perform certain drawing actions within this method, it will call itself (you might be able to avoid this using SuspendLayout and ResumeLayout on the listbox while updating), but am not sure.
Here is the kicker as far as I know it. Everytime this event is triggered, it is pretty much for every item in the list. (This might be useful in that you can de-color a previously selected item, so dont jump directly to what I am about to suggest without thinking it through). So, the DrawItemEventArgs has an index of the item being drawn. Using that, you can focus in on a specific item that you need to draw. That might help you from re-processing something that has already been processed (keep in mind the notes from the above article..copied below, about index being allowed to be -1).
To visualize this process:
Add 1 item ->DrawItem
Add 2nd item->DrawItem
->DrawItem
Add 3rd item->DrawItem
->DrawItem
->DrawItem
Add nth item->DrawItem * n
So, this actually results in a kind of fibonacci type situation (3 items resulted in 6 calls...5 would result in your 15 number), and you can see how the initial load could be cumbersome, and how adding a new item make for n calls to the method.
From the article above:
The ListBox calls the DrawItem method repeatedly, for each item in its
Items collection.
The DrawItemEventArgs argument to the DrawItem event handler exposes
an Index property whose value is the index of the item to be drawn.
Watch out! The system raises the DrawItem event with an index value of
-1 when the Items collection is empty. When that happens you should call the DrawItemEventArgs.DrawBackground() and DrawFocusRectangle()
methods and then exit. The purpose of raising the event is to let the
control draw a focus rectangle so that users can tell it has the
focus, even when there are no items present. The code traps for that
condition, calls the two methods, and then exits the handler
immediately

If a dialog box pops up over the top your control, guess what your control has to do when you close the dialog box? It has to redraw itself!
Try using Debug.Writeline to write debugging information when you are looking at drawing controls. Of course, you still need to be careful that you don't pull up Visual Studio over the top of your form as well! It's a situation where two monitors can really help.

Related

GridView doesn't scroll down to virtualized items

In my application, I need to select the newly created document(note) when I go back to library. After library item is selected, the Library must be scrolled to the selected item.
My library's OnLoaded method:
private async void OnLoaded(object sender, RoutedEventArgs e)
{
await this.ViewModel.InitializeAsync();
// CollectionViewSource of my GridView being filled
ViewModel.CollectionChanging = true;
GroupInfoCVS.Source = ViewModel.GroupsCollection;
ViewModel.CollectionChanging = false;
// Loading Last selected item - THIS CHANGES SELECTION
ViewModel.LoadLastSelection();
}
After I call the LoadLastSelection method, selection is changed successfuly (I've tested). This is the method that is called after that (in our GridView's extended control):
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.SelectedItemsCount = this.SelectedItems.Count;
var newlySelectedItems = e.AddedItems;
if (newlySelectedItems.Any())
{
var item = newlySelectedItems.Last();
ScrollTo(item);
}
}
private void ScrollTo(object item)
{
UpdateLayout();
var itemUI = (FrameworkElement)this.ContainerFromItem(item);
if (itemUI != null)
{
_scrollViewer.UpdateLayout();
_scrollViewer.ChangeView(null, itemUI.ActualOffset.Y - itemUI.ActualHeight, null, false);
}
}
This also works for the most part. When itemUI is not null, the method scrolls successfully to the required item. The problems start when the items start to overflow the screen size. When items are completely hidden from the screen, they are virtualized. That means that ContainerFromItem returns null, so I can't take the offset properties. Keep in mind that this actually occurs before Library's OnLoaded method is finished.
Please, help me with some alternative to get such properties or other methods of scrolling, which will help me scroll successfully.
I've read a lot and tried using Dispatcher.RunAsync and ScrollIntoView methods, but I couldn't manage to produce any scrolling behavior. If you point me how to use them successfully, that would be a nice help too.
Here's what I've read (and tried):
ItemContainerGenerator.ContainerFromItem() returns null?
How to Know When a FrameworkElement Has Been Totally Rendered?
Is there a "All Children loaded" event in WPF
Let ListView scroll to selected item
Thanks in advance!
IMPORTANT: If you don't want to read all the conversation within the official answer, please read the solution in short here:
TemplatedControl's style had changed ScrollViewer's name from "ScrollViewer" to "LibraryScrollViewer" and that rendered ScrollIntoView method useless.
For GridView, the best way to achieve your needs is to call GridView.ScrollIntoView.
But you seem to have made similar attempts, and it does not to be successful, then the following points may help you:
1. Don't use GridView as a child element of ScrollViewer.
In your code, I see that you are calling the method of ScrollViewer.ChangeView to adjust the view scrolling, so it is speculated that you may put the GridView in the ScrollViewer, which is not recommended.
Because there is a ScrollViewer inside the GridView, and its ScrollIntoView method is to change the scroll area of the internal ScrollViewer. When there is a ScrollViewer outside, the ScrollViewer inside the GridView will lose the scrolling ability, thus making the ScrollIntoView method invalid.
2. Implement the Equals method of the data class.
If your data class is not a simple type (such as String, Int32, etc.), then implementing the Equals method of the data class will help the GridView to find the corresponding item.
Thanks.

How to automatic add the first line of text from a list box to a text box

Right I have got a list box which contains a list of tracks and when the track is pressed it moves to a second list box, what I now need to happen is have the first item that's in the second list box move automatic to a text box.
This is my current code for the first move
private void genreListBox_DoubleClick(object sender, EventArgs e)
{
playlistListBox.Items.Add(genreListBox.SelectedItem);
}
I am thinking it should be something like this
presentlyPlayingTextBox.AppendText(playlistListBox.);
But I'm not sure how to add the first line without clicking it.
I have tried this but I get an error for the value.
presentlyPlayingTextBox.Text = playlistListBox.SelectedItem.Value;
Normally you would use something like the 'SelectedIndexChanged' or 'SelectedValueChanged' action of a listbox, otherwise events like DoubleClick will fail if the keyboard is used to change the selection.
Also that event should be triggered on the second listbox when it is changed by the first.
EDIT:
On a standard Listbox, there is no .SelectedItem.Value, only .SelectedItem.
However as a listbox holds objects, not just text, you need to say you want the text value.
presentlyPlayingTextBox.Text = playlistListBox.SelectedItem.ToString();
I don't know if I fully understand your question, but if you're just attempting to move the first item from ListBox_2 to a textbox, would the following work?
presentlyPlayingTextBox.Text = playlistListBox.Items[0]?.Value; // C# >= 6
presentlyPLayingTextBox.Text = ((playlistListBox.Items == null) ? playlistListBox.Items[0].Value : "" ); // C# < 6
You could also create your own delegate/event that would fire when the user did some action.

Which method/function is called during PivotItem navigation WP7

I am just getting started on Windows phone 7 development and am stuck at this problem while using Pivot control:
I have 3 pivotitems and the swipe movement to navigate between pivots are working fabulously well, But the problem is...
I need to call a different function, say function1() when one pivotitem is visible and then call a function say function2() as soon as user swipes to another pivotitem.
Is there any delegate method which handles this..?
Thanks for your help!
You can handle the Pivot control's LoadingPivotItem event. This event passes PivotItemEventArgs which includes a property letting you know what pivot is about to be shown. Using this, you can then load the relevant controls and properties. For example,
private void pivotMain_LoadingPivotItem(object sender, PivotItemEventArgs e)
{
if (e.Item == pivotItem1)
{
//Load Pivot Item 1 stuff
}
if (e.Item == pivotItem2)
{
//Load Pivot Item 2 stuff
}
}
In the example above, pivotItem1 and pivotItem2 are the names I've given each PivotItem so you can give whatever names you want to each PivotItem and check if they're about to be shown. If you want to handle the event after the PivotItem has loaded, you can use the Pivot.LoadedPivotItem method.
If you want to know which PivotItem is currently being displayed at any time, you can use the Pivot.SelectedIndex method. It's zero-based, so the first PivotItem will have an index of 0, the second will have 1 and so on.
You can use SelectionChanged. In this function you'll be able to check to see which PivotItem is the SelectedItem and choose which function you want to call.

Bring Winforms control to front

Are there any other methods of bringing a control to the front other than control.BringToFront()?
I have series of labels on a user control and when I try to bring one of them to front it is not working. I have even looped through all the controls and sent them all the back except for the one I am interested in and it doesn't change a thing.
Here is the method where a label is added to the user control
private void AddUserLabel()
{
var field = new UserLabel();
userContainer.Controls.Add(field);
SendLabelsToBack(); // Send All labels to back
userContainer.Controls[field.FieldName].BringToFront();
}
Here is the method that sends all of them to the back.
private void SendLabelsToBack()
{
foreach (var label in userContainer.Controls);
label.SendToBack();
}
Yeah, there's another way. The Controls.SetChildIndex() also changes Z-order. The one with index 0 is the one on top. Doesn't buy you anything though, BringToFront() uses this method.
Your SendLabelsToBack() method as given cannot work, it will also send the label to added to the back. But your next statement fixes that again.
Okay, that doesn't work, which means the BringToFront() method doesn't get executed. Look in the Output window for a "first chance exception" notification. As written, your SendLabelsToBack() will cause an exception if the user control contains any control other than a UserLabel. Also, set a breakpoint after the BringToFront() call and check the value of userContainer.Controls[0].Name when it breaks.
Controls' z-index is per-container.
If you call BringToFront on a control that is inside a container (such as a Panel), it will not bring the container to the front.
Therefore, the control will only go in front of other controls in that container.
To see what containers your controls are in, you can use the Document Outline pane in the View menu.
EDIT: Your userContainer control is probably behind a different control.
Have you tried Invalidate() after BringToFront()? BringToFront does not raise the Paint event
try this:
private void SendLabelsToBack()
{
foreach (var label in userContainer.Controls)
{
label.SendToBack();
label.Invalidate();
}
}
I think you just need to change your last line:
userContainer.Controls[field.FieldName].BringToFront();
to this:
userContainer.Controls[field.Name].BringToFront();
When you use a string as the indexer for the Controls collection, it goes by the Name property of the control (not the FieldName property).
Since you're just trying to bring the most recently-added control to the top, this would also work:
userContainer.Controls[userContainer.Controls.Count - 1].BringToFront();
From my experience looks like windows puts all controls belonging to one graphic container(pane, group box...etc) in a software collection. The collection is ordered by child index which is a property of every control in that container.
The trick is that children with the same index can and do exists. In this case windows will paint those children ordered relative to others but between them it will paint them in the reverse order they had been added to the container.
Long story short: for one container-you need to make sure controls have different indexes by changing ALL NOT just SOME of the indexes when you want to change the z-order. For example:
foreach (Control newControl in TopControl.Controls)
{
TopControl.Controls.SetChildIndex(newControl,indexlogic(newControl));
}
where indexLogic(newControl ) is your method of calculation of the index of particular control.

Redraw Toolstrip based on selections

I have been asked to write c# winforms app that will give users the ability to select options from a checkbox list and have it automatically redraw/repaint a toolstrip with the selected items.
I am new to winforms so I am not sure how to approach it. Should I be using the BackgroundWorker Process? Invalidate()?
Just alittle confused.
Any assistence of pointing in the right direction would be appreciated.
You probably don't want a BackgroundWorker as that's run on a non-UI thread and would cause problems when you try to modify the toolstrip (you can only work with the UI on the thread the UI was created on). Handle the CheckedChanged events on the checkboxes and then add or remove items from the toolstrip. The repainting should be automatic.
You need to keep tooltips for all options some where (if Tag property of checkboxes is free the put it there). Then when an option is selected or deselected, you need to update tooltips.
Let's suppose you are adding all the checkboxes in a IList. then things will work as follows:
private IList<CheckBox> options= new List<CheckBox>();
private void UpdateTTip()
{
toolTip1.RemoveAll();
foreach (CheckBox c in options)
{
if (c.Checked)
toolTip1.SetToolTip(c, c.Tag.ToString());
}
}
Now you need to call this on checkedchanged event of options check boxes:
private void chk_CheckedChanged(object sender, EventArgs e)
{
UpdateTTip();
}
A toolstrip contains controls by itself - it does not just "paint" buttons you can press. In order to have the toolstrip display different buttons depending on different conditions, you can:
Clear the toolstrip items and re-create the ones that are needed in the current context in code when items are checked in the list you mentioned
Add all the items and design time (with property Visible = false) and only set the necessary ones to Visible = true upon selection in your check listbox
No need to do any painting :-)

Categories