When I first saw Propellerheads Reason interface years ago, I was mesmerised by the cabling system. It seemed so intuitive to connect devices with each other. I rebuilded such an interface in RealBasic and was very satisfied with it. Lataly, I stumbled on this old project and I decided to give it a go in Xojo. It seemed like a good idea for an article on my blog.
For this article I wrote some classes (ABCable mainly) so it’s easy to implement in other projects in Xojo. I wrote a quick and dirty system to add and move cables cutting a couple of corners, as you may know I tend to do 🙂
When you want to use such a system, I urge you to rewrite that part in a more stable framework. But all the principles are here.
The Windows and OSX versions will also look slightly different. I did not find a good way to draw a smooth Catenary curve with the MacOSLib so I decided to use different functions for them, simulating the Windows version as much as possible. (Check DrawOSX and DrawWIN in the ABCable class to see what I’ve done). You can rewrite them as you please.
ABCable has three possible constructors:
' use for itaerations Constructor() ' the start and end point, the color, the minimum length of the cable (will not be exact), the cable thickness (experimental) Constructor(iStartPoint as ABPoint, iEndPoint as ABPoint, iCableColor as Color, iCableMinimumLength as double, iCableThickness as double ) ' is the same as above but with an additional parameter: iPointSpacing ' this param can be used to 'smoothen' the curve by considering more points. The default is 8 segments. Increasing this number will slow down the animation. Constructor(iStartPoint as ABPoint, iEndPoint as ABPoint, iCableColor as Color, iCableMinimumLength as double, iPointSpacing as Double, iCableThickness as double )
The other functions you’ll need from the ABCable class are:
Draw(DrawType as integer)
This function updates the graphics. I added two types of drawing. You can use DrawType to change it to the way you like:
DRAWBEZIER: Draw the curve as a Bezier curve
DRAWCATENARY: Draw the curve as a Catenary curve (for OSX it’s a simulation as described above)
The left one is as a Bezier, the other as a Catenary.
SetStartPoint(value as ABPoint) SetEndPoint(value as ABPoint)
With these functions you can change the the Start en End connection points of the cable.
All the rest is done automatically (the shadow of the cable, the length increases/decreases when needed, etc…)
Let’s get into the rest of the program using this class.
I’m using a trick we learned in a previous canvas lesson to find where cables are and if there are connection points. We need 3 pictures. Our background without cables, a picture with the possible connection points in yellow (&cFFFF00) and a third one with the current connections. This last one will be created within the program and changes dynamically. (I’ll go deeper into this last one later in this article)
I’ll go through some of the source code and explain each part as we encounter it.
First, setup manual double buffering as we did in all the other lessons (using pBuffer as Picture, gBuffer as Graphics) so that our paint event in the canvas can simply be:
Sub Paint(g As Graphics) if pBuffer <> nil then g.DrawPicture pBuffer,0,0 end if End Sub
In the open event of the Window, we’ll set everything up:
Sub Open() ' setup the buffer pBuffer = New Picture(Canvas1.Width, Canvas1.Height, 32) gBuffer = pBuffer.Graphics ' setup the connections bitmap CurrentConnections = New Picture(Canvas1.Width, Canvas1.Height, 32) ' try = DRAWBEZIER to see another effect DrawMethod = DRAWCATENARY ' Important! Set lastColor to 2 so even numbers, not = 0, are startpoints, odd numbers are endpoints ConnectionLastColor = 2 ' draw it DrawCables End Sub
We initialized our Buffer and also our CurrentConnections bitmap (the third picture from above). It has the same dimensions as the canvas (and background). We pick a Drawing method (here the Catenary type) and a help variable for identifying the start and end points of each cable we will draw. As the background is black, we’ll start at 2 so our starts will be even numbers (not being 0) and our odd numbers will be the end points. Finally, we draw the cables (in this case, none are defined, so just the background).
I’ll come back to the DrawCables function in a moment. First let’s check the canvas mouse events:
Function MouseDown(X As Integer, Y As Integer) As Boolean ' check if it is an existing cable dim c as ABCable c = CheckCables(X,Y) if c <> nil then BringToFront(c) DrawCables end if Return true End Function Sub MouseDrag(X As Integer, Y As Integer) if IsDraggingEnd then Cables(UBound(Cables)).SetEndPoint(new ABPoint(X,Y)) DrawCables me.MouseCursor = System.Cursors.InvisibleCursor Elseif IsDraggingStart then Cables(UBound(Cables)).SetStartPoint(new ABPoint(X,Y)) DrawCables me.MouseCursor = System.Cursors.InvisibleCursor end if End Sub Sub MouseUp(X As Integer, Y As Integer) if IsDraggingStart or IsDraggingEnd then dim col as Color col = CurrentConnections.Graphics.Pixel(X,Y) if col <> &c000000 then ' it's used by another one, cannot be connected Cables.Remove(UBound(Cables)) else ' is it a connection point? col = BackgroundConnections.Graphics.Pixel(X,Y) if col <> &cFFFF00 then ' not yellow, so can not be used to connect a cable Cables.Remove(UBound(Cables)) end if end if IsDraggingStart = false IsDraggingEnd = false DrawCables end if me.MouseCursor = System.Cursors.StandardPointer End Sub
In the MouseDown we’ll check if we have an existing cable or if we have to create a new one. This is done with the CheckCables() function. This function uses the CurrentConnections picture to check which, if any, cable you clicked on. We grab the pixel at postion X,Y and if it’s not black, we’ve touched a start or end point of a cable. If the color is even, we have a start point so we set IsDraggingStart = true. If it is odd, we’ll set IsDraggingEnd = true.
If it is black, we’ll check if it may be a possible connection point. We grab the pixel at position X,Y in the BackgroundConnections picture. If it is yellow, we can create a new Cable. We always assume you’re dragging the end of the cable to we’ll set IsDraggingEnd = true.
In both cases, we return the existing cable, the new cable or nil if you just clicked on the background. Check out the code to see how it works. I’ve added comments to make it easier to follow.
Function CheckCables(X as integer, Y as integer) As ABCable Dim c as ABCable dim col as Color dim colInt as Integer ' check if a cable is already connected to this connection point col = CurrentConnections.Graphics.Pixel(X,Y) if col <> &c000000 then ' it's an existing one for each c in Cables if c.StartPointColor = col or c.EndPointColor = col then ' found the cable colInt = ColorToInteger(col) if colInt mod 2 = 0 then ' is the start IsDraggingStart = true else ' is the end IsDraggingEnd = true end if Return c end if next else ' it's a new one, can it be connected col = BackgroundConnections.Graphics.Pixel(X,Y) if col = &cFFFF00 then ' yellow, so can be used to connect a cable dim i as Integer ' pick a random color for the cable dim NewCableColor as Color i = rnd*4 select case i case 0 NewCableColor = RGB(242, 131, 42) case 1 NewCableColor = RGB(97,149,102) case 2 NewCableColor = RGB(110,196,196) case 3 NewCableColor = RGB(206,73,78) end select ' create a new cable c = new ABCable(new ABPoint(X,Y), new ABPoint(X,Y), NewCableColor,400,30) ' set its start end end color for the CurrentConnections bitmap to trace the mouse c.StartPointColor = IntegerToColor(ConnectionLastColor) c.EndPointColor = IntegerToColor(ConnectionLastColor+1) ' update the new last color for the next cable ConnectionLastColor = ConnectionLastColor + 2 ' add the cable Cables.Append c ' always assume it's the end IsDraggingEnd = true end if end if Return c End Function
Now we return to the MouseDown() event. If we have a Cable (c not nil), bring this cable to the front and draw again.
When we pass through the MouseDrag() event, one of our ends (start or end) will be active. Depending on which one it is, we’ll adjust the position by using SetStartPoint and SetEndPoint of the last Cable in our stack. Because we used the BringToFront() function, the last one in the stack is always our current one.
In the MouseUp() event we’ll do something similar as in the CheckCables() function. Now we first check if nobody is yet connected to this connection point (CurrentConnections.Graphics.Pixel(X,Y) not &c000000). In this case, we’ll just remove the cable. If it is black, we’ll check if it is a possible connection point (BackgroundConnections.Graphics.Pixel(X,Y) not &cFFFF00 ‘ yellow). If it is something else than yellow, remove the cable also.
Reset the Dragging variables to false and Draw the cables.
In the DrawCables() function, we’ll do five things:
1. Setup our GDIPlusGraphics for windows or our CGContextGraphicsPort for OSX if not done yet
2. We draw the background
3. We reset the CurrentConnection picture
4. We’ll draw all the cables on it
5. We’ll draw the current Start and End points of the cables to our CurrentConnection picture
Sub DrawCables() '1. SETUP + 2. DRAW THE BACKGROUND #if TargetWin32 if WINGfx = nil then dim s as Status WINGfx = new GdiPlusGraphics( gBuffer.Handle( Graphics.HandleTypeHDC ) ) s = WinGfx.SetInterpolationMode(InterpolationMode.High) s = WinGfx.SetCompositingQuality(CompositingQuality.High) s = WinGfx.SetSmoothingMode(SmoothingMode.AntiAlias) end if gBuffer.DrawPicture BackGround,0,0 #else if OSXGfx = nil then OSXGfx = new CGContextGraphicsPort( gBuffer ) OSXGfx.SetSAllowsAntialiasing(true) OSXGfx.SetShouldAntialias(true) OSXGfx.TranslateCTM(0, gBuffer.Height) OSXGfx.ScaleCTM(1.0, -1.0) end if OSXGfx.SaveGState OSXGfx.TranslateCTM(0,gBuffer.Height) OSXGfx.ScaleCTM(1.0, -1.0) dim r as CGRect = CGRectMake(0,0,gBuffer.Width, gBuffer.Height) if ImageOSX = nil then dim f as FolderItem dim p as Picture f = GetFolderItem("Background.png") if f.Exists then ImageOSX = CGImage.NewCGImage(p.Open(f)) end if end if OSXGfx.DrawImage(ImageOSX, r) OSXGfx.RestoreGState #endif ' 3. CLEAR THE CURRENT CONNECTIONS CurrentConnections.Graphics.ForeColor = &c000000 CurrentConnections.Graphics.FillRect 0,0, CurrentConnections.Width, CurrentConnections.Height ' 4. DRAW ALL OUR CABLES + 5. DRAW OUR START AND ENDPOINTS dim c as ABCable for each c in Cables c.Draw(gBuffer, DrawMethod) ' if it's not the current cable if c <> Cables(UBound(Cables)) then CurrentConnections.Graphics.ForeColor = c.StartPointColor CurrentConnections.Graphics.FillOval(c.StartPoint.x-6, c.StartPoint.y-6, 12,12) CurrentConnections.Graphics.ForeColor = c.EndPointColor CurrentConnections.Graphics.FillOval(c.EndPoint.x-6, c.EndPoint.y-6, 12,12) else 'if it is, see if it is being dragged if not IsDraggingStart then CurrentConnections.Graphics.ForeColor = c.StartPointColor CurrentConnections.Graphics.FillOval(c.StartPoint.x-6, c.StartPoint.y-6, 12,12) end if if not IsDraggingEnd then CurrentConnections.Graphics.ForeColor = c.EndPointColor CurrentConnections.Graphics.FillOval(c.EndPoint.x-6, c.EndPoint.y-6, 12,12) end if end if next Canvas1.Refresh(false) End Sub
The result is this:
The first is the result in our Buffer. The second one is the result of the CurrentConnections Drawings. We’ll see the start and end points. The third picture is just to show you where each point in CurrentConnections comes from. So now you see when we use the CheckCables function, we can know exactly which cable and at what end we clicked by checking the pixel color of currentConnections.
Note: the colors in 2 and 3 are faked to make it clear how the principle works. In the real thing, all points are only one integer away from each other, but this is not very visible for us.
And that’s it! There are some help functions like IntegerToColor, ColorToInteger and an extra MouseMove event to hide the cursor but they need no explanation.
This was a fun project I think. Let’s look how it goes in this video:
You can download the full source code here:
This concludes the cable and chains tutorial.