[VB.NET] Shapes resizen, rekening houden met minimum size?

Pagina: 1
Acties:

Acties:
  • 0 Henk 'm!

  • NickThissen
  • Registratie: November 2007
  • Laatst online: 09-09 10:50
Hey,

Ik ben momenteel aan het werken aan een vrij simpele 'shape editor', vergelijkbaar met de winforms editor in Visual Studio.

Korte uitleg over wat ik tot nu toe heb:

Ik heb een abstracte klasse Shape, waar elke vorm (denk aan Rectangle, Ellipse, Triangle, etc) van zal overerven. Deze klasse heeft een aantal basis benodigdheden, zoals een Bounds property (alsook Location en Size, wat uit de Bounds gehaald kan worden) die de Rectangle aangeeft waar de shape zich bevind. Er is ook een Move methode en een Resize methode (daarover later meer).
De klasse heeft tenslotte uiteraard een abstracte Draw methode, die elke eigenlijke vorm zelf moet implementeren.

Daarnaast heb ik een klasse Canvas (erft over van Panel), wat in principe het 'tekenbord' is waar alle shapes op verschijnen. Deze klasse heeft een lijstje (List(Of T)) van shapes, en een SelectedShape property, die de huidig geselecteerde Shape bevat (op het moment is er geen mogelijkheid om meerdere shapes tegelijk te selecteren).

In de OnPaint methode van de Canvas wordt met een simpel loopje door de shapes List van elke shape de Draw methode aangeroepen. Naast de Draw methode wordt ook de DrawDragHandles methode aangeroepen, die een selectie + vierkantjes in de hoeken tekent, net als in de form designer van Visual Studio.


Om de shapes te kunnen manipuleren gebruik ik de verscheidene OnMouse____ overrides van de Canvas klasse. Bij OnMouseDown bijvoorbeeld wordt gekeken of de muisklik in een van de 'drag handles' is (of aan de zijkant), wat dus tot resizen moet leiden, of ergens in het midden, wat dus voor een move moet zorgen. Om dit onderscheid te maken is er een enum HitStatus, met waarden als None (buiten de shape geklikt), Drag, ResizeLeft, ResizeTopLeft, Resize___ etc, voor elke richting.

Als de muis ingedrukt wordt, dan wordt een van deze keuzes in een globale 'hitStatus' variabele opgeslagen, zodat ik in de OnMouseMove kan kijken wat ik moet doen (verplaatsen of resizen).

In de OnMouseMove gebeurt dus dit:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Protected Overrides Sub OnMouseMove(ByVal e As MouseEventArgs)
   MyBase.OnMouseMove(e)

   If mouseDown Then
      If Me.SelectedShape IsNot Nothing Then

         If hitStatus = Shape.HitStatus.Drag Then  'we moeten verplaatsen

            Me.SelectedShape.Move(e.X - moveStart.X, e.Y - moveStart.Y)
            moveStart = e.Location
         
         ElseIf hitStatus <> Shape.HitStatus.None Then  'we moeten resizen

            Me.SelectedShape.Resize(hitStatus, e.X - resizeStart.X, e.Y - resizeStart.Y)              
            resizeStart = e.Location

         End If
 
      End If
   End If
End Sub


De Move methode is simpel: de Bounds krijgt gewoon een offset.

De Resize methode is ook niet super ingewikkeld, maar wel wat lang. Ik moet natuurlijk voor elke richting apart de Bounds aanpassen. Dat doe ik dus zo:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Public Sub Resize(ByVal hitStatus As HitStatus, ByVal dx As Integer, ByVal dy As Integer)

   Dim newBounds As Rectangle = Me.Bounds

   Select Case hitStatus
      Case HitStatus.TopLeft
         newBounds = New Rectangle(newBounds.X + dx,
                   newBounds.Y + dy, 
                   newBounds.Width - dx,
                   newBounds.Height - dy)
      Case HitStatus.Bottom
         newBounds = New Rectangle(newBounds.X,
                   newBounds.Y, 
                   newBounds.Width,
                   newBounds.Height + dy)
      'etc, voor elke mogelijke richting
   End Select
 
   Me.Bounds = newBounds

End Sub



Dit werkt allemaal prima. Ik kan de shapes verplaatsen en resizen zonder problemen.

Maar nu wil ik graag de mogelijkheid hebben om de shapes een MinimumSize te geven, waar ze dus niet onder mogen komen. Ik heb alleen geen idee hoe ik dit correct moet implementeren...

Mijn eerste, "naive", oplossing was een simpele check op het eind van de Resize methode: als de Height groter is dan de minimum Height, dan zet ik de newBounds.Height terug naar de minimum height (en hetzelfde voor de Width). Dit kan ik natuurlijk makkelijker schrijven met de Math.Max functie:
code:
1
2
3
newBounds.Height = Math.Max(Me.MinimumSize.Height, newBounds.Height)
newBounds.Width = Math.Max(Me.MinimumSize.Width, newBounds.Width)
Me.Bounds = newBounds


Hoewel dit in principe werkt (de shape wordt nooit kleiner dan het minimum), zijn er twee problemen:


1. Wanneer je de shape tot de minimum grootte resized, en dan toch met je muis door blijft bewegen, dan gebeurt er iets raars als je de muis daarna weer terug beweegt (met de knop nog steeds ingedrukt). De shape begint meteen weer met resizen, ook al is de muis nog lang niet over de 'grens' heen.

2. Wanneer je, bijvoorbeeld, de shape van de linkerkant kleiner maakt, dan begint de shape te verplaatsen (naar rechts) zodra je het minimum bereikt.

Ik snap dat dit waarschijnlijk onbegrijpelijk is, dus vandaar een kort filmpje met het probleem:
http://www.youtube.com/watch?v=gPg8ZK2KJI8

Eerst zie je probleem 1 (de shape wordt weer groter terwijl de muis nog ergens links-boven de shape zweeft), en daarna probleem 2.


Wat probleem 1 veroorzaakt snap ik nog niet helemaal...

Voor probleem 2 denk ik dat dit te maken heeft met mijn, te naive, implementatie van de minimum size. Als er van links geresized wordt bijvoorbeeld, dan verander ik de newBounds op deze manier:
code:
1
2
3
4
newBounds = New Rectangle(newBounds.X + dx,
              newBounds.Y,
              newBounds.Width - dx,
              newBounds.Height)

Ik maak dus niet alleen de breedte kleiner, maar ik verplaats de shape ook naar rechts (door de X coordinaat dx groter te maken). Als ik dat niet doe dan resized de shape natuurlijk gewoon van rechts, omdat een Rectangle nu eenmaal vanuit de linker-bovenhoek gedefinieerd is.

Maar als de breedte nu te klein wordt, dan wordt die gewoon gereset. De verplaatsing (X + dx) wordt echter niet meer terug gezet, dus verplaatst de shape wel...

Dit heb ik geprobeerd op te lossen door de check op het einde zo te doen:
code:
1
2
3
4
5
6
7
8
If newBounds.Height < Me.MinimumSize.Height Then
   newBounds.Height = Me.MinimumSize.Height  'reset height
   newBounds.Y = Me.Bounds.Y  'reset location
End If
If newBounds.Width < Me.MinimumSize.Width Then
   newBounds.Width = Me.MinimumSize.Width  'reset height
   newBounds.X = Me.Bounds.X  'reset location
End If

Dit werkt al ietsje beter, maar nog steeds niet optimaal. Nu zie je de shape soms wat verspringen, geen idee waarom...


Heeft iemand enig idee hoe ik een minimum size correct implementeer? Is mijn hele resize implementatie 'te makkelijk', is er een betere manier?

Hier moet toch wel iets over bekend zijn, aangezien er zoveel programma's zijn die zoiets gebruiken (denk aan alle Office producten, Visual Studio, etc...) en daar werkt het wel allemaal prima.

Bedankt!

Mijn iRacing profiel


Acties:
  • 0 Henk 'm!

  • epic007
  • Registratie: Februari 2004
  • Laatst online: 25-08 11:27
Je gebruikt voor het resizen een deltax en deltay zie ik. Wanneer je de muisknop ingedrukt houdt en je dx en dy worden weer positief dan wordt automatisch je shape weer groter gemaakt terwijl de muispointer totaal ergens anders staat.
Misschien moet je je hoekpunten tijdens het resizen gewoon de waarde van de muiscursor geven en daarna checken of dit een legale waarde is.

Acties:
  • 0 Henk 'm!

  • NickThissen
  • Registratie: November 2007
  • Laatst online: 09-09 10:50
Dat klinkt inderdaad als een oplossing. Op dit moment reken ik gewoon de dx en dy waarden uit door de vorige muis-positie van de huidige muis-positie af te trekken. Dat verschil tel ik dan bij de breedte/hoogte/x-positie/y-positie op (afhankelijk van welke richting de resize op gaat).

Ik weet alleen niet zeker hoe ik dat zou moeten implementeren. Ik kan vanavond wel wat experimenteren als ik thuis ben, maar zou je misschien een globale uitleg kunnen geven?

Je zegt dus dat ik het hoekpunt dat 'vastgehouden' wordt met de muis gewoon op de nieuwe positie van de muis neerzet (tenzij dat niet legaal is). Maar resizen gebeurt niet alleen met de hoekpunten; je kan ook ergens tussen twee hoekpunten in 'grijpen' (net zoals je een venster in windows kan resizen). Dus daar moet ik dan wel rekening mee houden! Ik zou zo gauw even niet weten hoe ik dat moet doen?

Mijn iRacing profiel


Acties:
  • 0 Henk 'm!

  • Feanathiel
  • Registratie: Juni 2007
  • Niet online

Feanathiel

Cup<Coffee>

Opzich is dat laatste niet zo'n probleem. Dat is namelijk hetzelfde als je ervoor kiest om slechts één kant te resizen. Wanneer je bijvoorbeeld uitgaat van de draghandle rechts onderin en de rand erboven probeert te 'verplaatsen', dan zul je alleen berekeningen op de X-as hoeven te verrichten.

Uitgaande van deze draghandle rechts links onderin, zou je het volgende voorbeeld kunnen gebruiken. Daarin wordt er een berekening gedaan waarbij er gebruik wordt gemaakt van een minimum breedte. Dit voorbeeld is uitgewerkt in JavaScript, maar dat zou op zich hetzelfde moeten werken in VB:

JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var newLeft = mouseX; // hier maak je gebruik van de positieX van de muis, geen dX zoals hierboven al vermeld
var newWidth = right - newLeft; // right = left + width
var newHeight = mouseY - top; // ook hier, positieY en geen dY

if( newWidth < minWidth )
{
    // De breedte wordt hier terug gezet, maar let ook vooral op de locatie van de linkercoordinaat.
    // Deze zal opnieuw moeten worden berekend.
    newLeft = right - minWidth;
    newWidth = minWidth;
}

if( newHeight < minHeight )
{
    newHeight = minHeight;
}

// top blijft hetzelfde
left = newLeft;
width = newWidth;
height = newHeight;


Mocht je de gehele uitwerking willen hebben (alle hoekpunten). Laat het dan even weten. Ook deze heb ik uitgewerkt voor mijzelf (als uitdaging). Maar ik wil je zelf de kans geven om er wat van te leren, en als het goed is, zou je een heel eind moeten kunnen komen met het bovenstaande voorbeeld.

Edit:
@hieronder: mijn excuus, ik heb het nog even aangepast voor toekomstige referentie.

[ Voor 3% gewijzigd door Feanathiel op 26-05-2010 10:18 . Reden: Foutieve handle (links ipv rechts) ]


Acties:
  • 0 Henk 'm!

  • NickThissen
  • Registratie: November 2007
  • Laatst online: 09-09 10:50
Ok, dat kan ik thuis eens uitproberen vanavond. Ik zie wel al dat het een hele lap code wordt, wat ik eigenlijk zoveel mogelijk wil voorkomen. Maargoed, als er geen andere mogelijkheid is dan moet het maar.

EDIT
Het lijkt prima te werken (hoewel je voorbeeld voor links-onder was ipv rechts-onder ;)). Het is wat veel code, maar die gooi ik dan maar in aparte methods, dan valt het nog wel mee.

Bedankt!

[ Voor 33% gewijzigd door NickThissen op 25-05-2010 19:07 ]

Mijn iRacing profiel