Best way to make a "tile selector" box?
Best way to make a "tile selector" box?
Best way to make a "tile selector" box?
Best way to make a "tile selector" box?
Best way to make a "tile selector" box?
Best way to make a "tile selector" box? Best way to make a "tile selector" box? Best way to make a "tile selector" box? Best way to make a "tile selector" box? Best way to make a "tile selector" box? Best way to make a "tile selector" box? Best way to make a "tile selector" box? Best way to make a "tile selector" box?
Best way to make a "tile selector" box? Best way to make a "tile selector" box?
Best way to make a "tile selector" box?
Go Back  Xtreme Visual Basic Talk > > > Best way to make a "tile selector" box?


Reply
 
Thread Tools Display Modes
  #1  
Old 01-16-2011, 05:31 PM
WhatsMyUsername's Avatar
WhatsMyUsername WhatsMyUsername is offline
Regular
 
Join Date: Jan 2011
Location: Currently? Costa Rica
Posts: 52
Default Best way to make a "tile selector" box?


So now I will try to make something more complex, still playing around with this graphics thing.

I want a picturebox to display a tileset (a bunch of 32x32 tiles normally used for creating game levels etc), the picturebox is inside a container (which has autoscroll ON). The picturebox is larger than the container, so the container will make scrollbars automatically.

Great, it is working for now.

Now, I need to be able to choose a tile or multiple tiles (if I click & drag the mouse)

Basically I want to draw a 32x32 "red" box over each selected tile (so the user knows what tiles are selected).

So first of all, I need to know when the mouse clicks the picture. Good. It works.
Now, I need to know which 32x32 tile was clicked. Well, I think I can calculate that just fine... BUT, what if the user drags the mouse while clicking on a tile? I want to support multiple tiles selected, but I am clueless about tracking what tiles were chosen...

If we solve that problem, I would have another curiosity: You know the "red boxes" that tell you which tiles are selected? What would happen if I dragged the scroll bar? I need the redboxes to move along with the picture, and hide if they go to an invisible area.
Reply With Quote
  #2  
Old 01-17-2011, 10:43 AM
AtmaWeapon's Avatar
AtmaWeaponBest way to make a "tile selector" box? AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,500
Default

Properly explaining this concept is a tall order, most likely a multi-hour investment. I have a plan for a demonstration, but doubt I have the time to see it through today. Maybe someone else will pick it up.

Part of the problem is I take from your other posts you're a relative newbie to drawing in Windows Forms. That requires more in-depth explanation. The other part of the problem is you're asking several questions that are in and of themselves their own topic. I'll try a summary with very little code; if part of it confuses you ask about it and we'll see if I can skip some of the detailed explanation.

First, stop thinking of everything in terms of a picturebox. Or rather, don't believe it's the only way you can draw on a form. It's convenient to draw to a bitmap and let the picturebox display it. It's particuarly convenient if you're only ever going to be adding to the image. When you're doing more complicated stuff (like moving a focus rectangle around) it becomes wasteful. You have to keep a full bitmap of the entire region in memory. If you're loading tiles to draw, odds are you have those in memory too, so you're paying twice for the memory. It's much better to draw directly on the form or pick a lightweight control (like Panel) to represent the bounds of the drawing area. I'm not going to say you should *never* use a picturebox, but I write custom controls for a living and I never use one.

(You can get autoscroll from a Panel as well as a picturebox, but I'm not quite certain if it works the same. This might be a good reason to keep the Picturebox for your project. You can write your own scroll code but it is likely overkill.)

Figuring out which tile is clicked is easy, it just involves some 2D math. You have to know the bounds of your grid, or at least where it starts. We'll call it (10, 10) so we have a non-zero starting point. You need to know the size of each cell. We'll say you've got 24x24 tiles with no border between them, 5 rows, and 5 columns. From this, you can calculate the bounds of every cell: the first cell is from (10, 10) to (33, 33). There's two ways you can go about this.

You can pre-calculate the bounds of all squares and use that for hit-testing. For example, if you have an array of Rectangle objects, you can use their Contains() method to determine if a mouse click falls within the rectangle. This is kind of shaky when scrolling is involved, because you have to adjust either the click point or the rectangle bounds for hit detection and you certainly have to adjust the bounds when drawing. I usually do this when I'm not scrolling, but haven't tried it with scrolling.

You can also calculate which square was clicked on the fly. This tends to go better with scrolling. You use formulas to deduce rows and columns. The formulas look something like this:
Code:
row = Math.Floor((mouseY - startY) / cellHeight)
column = Math.Floor((mouseX - startX) / cellWidth)
You can check this. Using our coordinates above, let's see what row/column some clicks get us:
Code:
(11, 11) should get us (0, 0):
row = Floor((11 - 10) /  24) = Floor(1 / 24) = 0
column = Floor(1 / 24) = 0

(32, 50) should get us (0, 1):
row = Floor((50 - 10) / 24) = Floor(40 / 24) = 1
column = Floor((32 - 10) / 24) = Floor(22 / 24) = 0
This is nice because it plays well with scrolling. If you consider the scroll to be a pixel offset, you can just manipulate the click coordinates. Consider it this way. Suppose you have a 100x100 area and a 10x10 window. If you scroll down 30 pixels, a (0, 0) mouseclick is actually at (0, 30) in the full area. So, assuming you have some scrollOffset point you can calculate, you can use these formulas:
Code:
row = Floor((mouseY + scrollOffset.Y - startY) / cellHeight)
column = Floor((mouseX + scrollOffset.X - startX) / cellHeight)
I haven't checked those and don't do scrolling code much, so feel free to call me on that.

Now, what about detecting selected boxes? To do that, I'd go back to having a 2D array of some class. Depending on your drawing code, it might help to have one of these anyway. Suppose you have this class:
Code:
Class Tile
    Public Property TileImage As Image
    Public Property IsSelected As Boolean
End Class
Now your drawing code just has to check to see if each tile is selected. The drawing code might look something like this:
Code:
For row As Integer = 0 To Rows - 1
    Dim yCoordinate As Integer = row * rowHeight + (startY + scrollOffset.Y)
    For column As Integer = 0 To Columns - 1
        Dim xCoordinate As Integer = column * columnWidth + (startX + scrollOffset.X)
        Dim tile As Tile = _tiles(row, column)
        e.Graphics.DrawImage(tile.TileImage, xCoordinate, yCoordinate)
        If tile.IsSelected
            e.Graphics.FillRectangle(highlightBrush, xCoordinate, yCoordinate, columnWidth, rowHeight)
        End If
    Next
Next

...
highlightBrush isn't defined in that code. You have to decide what kind of brush you want to use for the highlight. I'm assuming you'll pick some semitransparent solid color. How do you make those move with the scrolling? This is where a picturebox would help: it'd be automatic so long as you're drawing to the *image* rather than the picturebox. If you are drawing directly to a control, you'd probably want to invalidate as you scroll. That might lead to flicker, but you might find some clever ways to avoid it. I want to play around with it because I've never really done a good scrolling control, but I've already spent too long on the forums today

Also note none of this was tested in VS. If you try a formula and it's not working, I could be wrong. If someone corrects it, they're probably right. Grid math is something I do well, but I never really do it with scrolling this directly so I'm not sure if I got it just right.
__________________
.NET Resources
My FAQ threads | Tutor's Corner | Code Library
I would bet money 2/3 of .NET questions are already answered in one of these three places.
Reply With Quote
  #3  
Old 01-17-2011, 11:38 AM
OnErr0r's Avatar
OnErr0rBest way to make a "tile selector" box? OnErr0r is offline
Obsessive OPtimizer

Administrator
* Guru *
 
Join Date: Jun 2002
Location: Debug Window
Posts: 13,774
Default

This sounds a lot like something a listview might handle very nicely. Just use the correct view and you have tiles with selection/multiselection etc. Obviously there's no red selection, but you should be able to draw that by subclassing.

Btw, if you want to calculate row/col you can normally ditch all the Math.Floor calls and use integer division instead.
__________________
Quis custodiet ipsos custodues.
Reply With Quote
  #4  
Old 01-17-2011, 05:25 PM
WhatsMyUsername's Avatar
WhatsMyUsername WhatsMyUsername is offline
Regular
 
Join Date: Jan 2011
Location: Currently? Costa Rica
Posts: 52
Default

Very, very interesting, and thanks a lot. I believe I am improving
One small question thought, when I select a brush or pen (dunno the difference actually) color, how do I set the transparency? Thanks ~
Reply With Quote
  #5  
Old 01-17-2011, 09:39 PM
AtmaWeapon's Avatar
AtmaWeaponBest way to make a "tile selector" box? AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,500
Default

The built-in brushes are fully opaque. They are all instances of SolidBrush, which can be created with a color you specify. Color.FromArgb() lets you specify an alpha value. Note that since SolidBrush implements IDisposable, you'll want to make sure you dispose of it. The Using statement helps.

This would draw a rectangle using a half-transparent red brush:
Code:
Dim halfTransparentRed As Color = Color.FromArgb(128, 255, 0, 0)
Using selectionBrush As New SolidBrush(halfTransparentRed)
    e.Graphics.FillRectangle(selectionBrush, 0, 0, 32, 32)
End Using
__________________
.NET Resources
My FAQ threads | Tutor's Corner | Code Library
I would bet money 2/3 of .NET questions are already answered in one of these three places.
Reply With Quote
  #6  
Old 01-17-2011, 11:20 PM
WhatsMyUsername's Avatar
WhatsMyUsername WhatsMyUsername is offline
Regular
 
Join Date: Jan 2011
Location: Currently? Costa Rica
Posts: 52
Default

Thanks!

By the way, I noticed you guys make a lot of use of Dim and Using keywords for all of these operations. Is that necessary? Because this works just fine:
Code:
g.FillRectangle(New SolidBrush(Color.FromArgb(128, 255, 0, 0)), rect)
Or am I doing something reaaaally stupid 0.o? Or you guys do that just for the sake of explanation?
Reply With Quote
  #7  
Old 01-18-2011, 08:39 AM
AtmaWeapon's Avatar
AtmaWeaponBest way to make a "tile selector" box? AtmaWeapon is offline
Fabulous Florist

Forum Leader
* Guru *
 
Join Date: Feb 2004
Location: Austin, TX
Posts: 9,500
Default

I wouldn't call it stupid, but it's certainly not the best practice.

First, there's the matter of memory management. .NET is a managed language, which means it does all of the memory management for you. To this end, there is a program called the Garbage Collector (GC) that periodically checks for objects that are no longer referenced and frees their memory. When you're working with managed objects, memory leaks are not supposed to be possible. (There are some scenarios where you can accidentally make objects that can't be garbage collected, but we'll ignore them for now.) There's one problem: many tasks are accomplished by using the Windows API from .NET. API code is unmanaged, which means you have to explicitly allocate and de-allocate all memory, and if you do not de-allocate the memory it isn't released until your program terminates. When a .NET object references some unmanaged memory, it usually implements IDisposable to provide a mechanism by which you can release its unmanaged memory.

There's a golden rule about the GC that exemplifies why it's not good enough to let it handle everything: The GC runs when it wants to and cleans up what it can. You cannot make assumptions about when the GC runs. You can force it to run, but if it decides there's not enough memory usage to justify a pass it won't do anything. This is because the GC can be a big performance drain and it only runs if it feels the benefits of a collection outweigh the costs.

If an object implements IDisposable, you should always call its Dispose() method as soon as you are done with it. Many Windows API objects consume a good deal of memory or are limited in number by the system. If you call Dispose(), you immediately release the unmanaged resources. If you wait for the GC to run, the resources are released whenever the GC gets around to it (maybe never.)

The safest way to work with a disposable object is to write code like this:
Code:
Dim foo As SomeDisposableType = Nothing
Try
    foo = New SomeDisposableType()
    foo.DoSomething()
Finally
    If foo IsNot Nothing Then
        foo.Dispose()
    End If
End Try
The Finally clause will always execute, even if an error occurs. This ensures that if the object is created, it is always disposed. Obviously, it's a bit tedious, so the Using clause was created to reduce the work; the following code is identical to the last sample:
Code:
Using foo As New SomeDisposableType()
    foo.DoSomething()
End Using
I tend to use Dim a lot in my posts because it makes things more clear by making each line of code do one thing. Let's discuss how to read two pieces of code and pick a more difficult color like "Medium Sea Green" (#426F42):
Code:
Dim bounds As New Rectangle(...)
Dim semiTransparentGreen As Color = Color.FromArgb(128, 66, 111, 66)
Using greenBrush As New SolidBrush(semiTransparentGreen)
    e.Graphics.FillRectangle(greenBrush, bounds)
End Using
That code is clear in what it does: it defines bounds, creates a greenish color, creates a brush based off of the color, and fills a rectangle. Now let's compare it to fitting everything on one line:
Code:
e.Graphics.FillRectangle(New SolidBrush(Color.FromArgb(128, 66, 111, 66), New Rectangle(0, 0, 32, 32))
Even though it does the same thing, it's much harder to read. You see that you're filling a rectangle; it's being filled by a brush; that brush is using a color made by some combination of ARGB; the rectangle's bounds are given. You have to read all of that just to figure out what's going on. On the other hand, you only need one line of my code to know everything:
Code:
e.Graphics.FillRectangle(greenBrush, bounds)
It's filling a rectangle with a green brush; if you need more details you go see how the variables were created.

(Also note your line of code never disposes of the SolidBrush, thus the resources it uses aren't released.)

Here's another example of how that kind of code can get you in trouble. Suppose you're writing some kind of intensity graph, and you need to fill several hundred "pixels" with certain colors. You might write it like this:
Code:
For row As Integer = 0 To 499
    For column As Integer = 0 To 499
        e.Graphics.FillRectangle(New SolidBrush(GetColor(row, column)), GetBounds(row, column))
    Next
Next
No matter how many possible colors you define, this will create 2,500 individual brushes. If there's only 10 colors, you wasted 2,490 brushes worth of resources. As the number of brushes increases, the odds that GDI won't let you have another increases. You could instead write it like this:
Code:
For row As Integer = 0 To 499
    For column As Integer = 0 To 499
        Dim targetColor As Color = GetColor(row, column)
        Using pixelBrush As New SolidBrush(targetColor)
            e.Graphics.FillRectangle(pixelBrush, GetBounds(row, column))
        End Using
    Next
Next
This only creates one brush at a time and disposes of it as soon as possible. This might create a memory penalty, so you might instead choose to keep a cache of brushes; so long as the number of objects you cache is relatively small it shouldn't be a problem.

So, in summary:
  • Creating more variables and using more lines of code often makes it easier to read and understand the code. You write code once and read it many times; optimize for readability first.
  • Anything you create* that implements IDisposable() should have its Dispose() method called as soon as you can**. That need not mean "every loop iteration" if you know you'll use the same one in a later iteration. Just make sure you eventually call Dispose().
  • Less lines of code does not imply good code.

* This is important. If something else creates the object, it's usually responsible for disposing of it. For example, Graphics implements IDisposable, but you don't dispose of the one in PaintEventArgs because the object that raises the event is responsible for disposing of it. When in doubt, check documentation or ask someone else.

** Just to be contrary, the dispose pattern lets objects define a Close() method when it makes more sense (like for file I/O objects.) By contract this is supposed to do the same thing as Dispose(), but several objects break this rule. If you see Close() call it. Maybe call them both; by contract there should be no harm in disposing twice.
__________________
.NET Resources
My FAQ threads | Tutor's Corner | Code Library
I would bet money 2/3 of .NET questions are already answered in one of these three places.
Reply With Quote
Reply


Currently Active Users Viewing This Thread: 1 (0 members and 1 guests)
 
Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is Off
HTML code is Off

Forum Jump

Advertisement:





Free Publications
The ASP.NET 2.0 Anthology
101 Essential Tips, Tricks & Hacks - Free 156 Page Preview. Learn the most practical features and best approaches for ASP.NET.
subscribe
Programmers Heaven C# School Book -Free 338 Page eBook
The Programmers Heaven C# School book covers the .NET framework and the C# language.
subscribe
Build Your Own ASP.NET 3.5 Web Site Using C# & VB, 3rd Edition - Free 219 Page Preview!
This comprehensive step-by-step guide will help get your database-driven ASP.NET web site up and running in no time..
subscribe
Best way to make a "tile selector" box?
Best way to make a "tile selector" box?
Best way to make a "tile selector" box? Best way to make a "tile selector" box?
Best way to make a "tile selector" box?
Best way to make a "tile selector" box?
Best way to make a "tile selector" box? Best way to make a "tile selector" box? Best way to make a "tile selector" box? Best way to make a "tile selector" box? Best way to make a "tile selector" box? Best way to make a "tile selector" box? Best way to make a "tile selector" box?
Best way to make a "tile selector" box?
Best way to make a "tile selector" box?
 
Best way to make a "tile selector" box?
Best way to make a "tile selector" box?
 
-->