Beginners OOP with AHK

Helpful script writing tricks and HowTo's
User avatar
JnLlnd
Posts: 487
Joined: 29 Sep 2013, 21:29
Location: Montreal, Quebec, Canada
Contact:

Re: Beginners OOP with AHK

17 Dec 2018, 15:44

As I said earlier, as a tutorial homework, I added some methods to the FileSystem class in order to store the files and directory objects in an "Items" container inside the "FileContainer" sub class.

I defined the "__New()" method for the "FileContainer" sub class with two parameters: 1) the path of the drive (like C:\) or folder to add and 2) the number of levels of sub folders to scan. The "__New()" method is calling a new custom method named "getFilesInFolder()" that will collect the files and folders under the root path.

Code: Select all

	class FileContainer extends FileSystem.FileSystemElement {
		__New( path, recurseLevels := 1 ) {
			; path -> folder or drive root to scan
			; recurseLevels -> number of folder levels to scan (default 1 this path only, -1 to scan all, 0 to scan none)
			if ( !fileExist( path ) )
				Throw exception( "Path """ . path . """ doesn't exist", "__New", "Exist test returned false" )
			if ( !inStr( fileExist( path ), "D" ) ) ; if file is not a folder or a drive
				Throw exception( "Error creating File", "__New", "Path does not points to Folder" )
			this.name := path
			if ( SubStr( path, 0, 1 ) = "\" ) ; remove ending backslash for drive roots (like "C:\")
				path := SubStr( path, 1, StrLen( path ) - 1 )
			if !( this.getFilesInFolder( path, recurseLevels ) )
				Throw exception( "Error getting Items", "__New", "Could not get items from container" )
		}
The custom method named "getFilesInFolder()" receives the same two parameters: 1) "thisPath" is the path of folder to scan and 2) "recurseLevels" is the number of levels of sub folders to scan. For each item found in "thisPath", the method "addItem()" is called to add the object returned by the "new FileSystem.Directory()" or "new FileSystem.File()" call.

In addition, for sub folders, the method "getFilesInFolder()" is called recursively for the sub folder with the "recurseLevels" decremented to keep track the number of levels scanned. The recursion continues as long as "recurseLevels" is positive. It stops when it gets down to zero. And if "recurseLevels" starts -1 (and lower with recursive calls), scan continues until the end of the directory branch.

Code: Select all

		getFilesInFolder( thisPath, recurseLevels ) {
			; thisPath -> folder or drive root to scan
			; recurseLevels -> number of folder levels to scan including this one
			if ( showToolTip )
				ToolTip, Getting files from:`n%thisPath%
			; if recurseLevels > 0 continue with sub folder
			; if recurseLevels < 0 continue until the end of branch
			; if recurseLevels = 0 stop recursion
			if ( recurseLevels = 0 )
				return true
			this.Items := Object() ; create an object to contain items (files and folders)
			Loop, Files, % thisPath . "\*.*", FD ; do not use "R" here, the class does the recursion below
			{
				if A_LoopFileAttrib contains H,S ; skip hidden or system files
					continue
				if A_LoopFileAttrib contains D ; this is a folder, create Directory object and recurse to sub level
					objItem := new FileSystem.Directory( A_LoopFileFullPath, recurseLevels - 1 ) ; "- 1" to track the number of levels
				else ; this is a file, create File object
					objItem := new FileSystem.File( A_LoopFileFullPath )
				this.addItem(objItem) ; add Directory or File object to Items container
			}
			return true
		}
The method "addItems()" is called to add the new "File" or "Directory" object to the "Items" array created in the "FileContainer" class. New items are added at the end or the array with a counter starting at 1.

Code: Select all

		addItem(objItem) {
			this.Items.InsertAt(this.Items.Length()+ 1, objItem) ; add Directory or File object to Items container
		}
The last new custom method "listFiles()" is called recursively to gather the list of items (folders and files) in the FileContainer. By default "listFiles()" returns all items in the container but parameters allow to pass a filter and select the number of recursion levels. Example are added to the full source below.

Code: Select all

		listFiles( filter := "", recurseLevels := -1 ) {
			; filter -> exclude items with filter in their name, default empty (include all items)
			; recurseLevels -> number of folder levels to scan (default -1 to scan all, 0 to scan none)
			; if recurseLevels > 0 continue with sub folder
			; if recurseLevels < 0 continue until the end of branch
			; if recurseLevels = 0 stop recursion
			if (recurseLevels = 0)
				return
			thisList := ""
			for intKey, objItem in this.Items {
				if !StrLen(filter) or InStr(objItem.name, filter)
					thisList .= objItem.name . "`n"
				if ( objItem.HasKey( "Items" ) ) ; this is a container, recurse
					thisList .= objItem.listFiles( filter, recurseLevels - 1 ) ; "- 1" to track the number of levels
			}
			return thisList
		}
These changes can be tested with the following code. After having created your own instance of the "FileSystem" class, create a new instance of the "Directory" sub class for folders or of the "Drive" sub class for drives with the appropriate path and levels parameters. In both case, the "__New()" method of the parent class "FileContainer" is called with these parameters. There are various examples of parameters in the full sources. Then, the "listFiles()" method is called to gather the content of the "Items" array. This data is showed in a simple Gui.

Code: Select all

global showTooltip := True

MyFileSystem := new FileSystem ; create my instance of the class
MyNewContainer := new MyFileSystem.Directory(  ) ; create a container for the specified folder and get files in this folder only

if ( showTooltip )
	ToolTip ; hide last tooltip

; get content from the MyNewContainer object
str := MyNewContainer.listFiles() ; get the list of files and folders contained in the MyNewContainer object

; show content in Gui
Gui, Add, Edit, w800 r25 ReadOnly, % SubStr( str, 1, 30000 ) . ( StrLen( str ) > 30000 ? "`n..." : "" ) ; limit because of 32k limit of Edit control
Gui, Add, Button, default, Close
GuiControl, Focus, Close
Gui, Show

return

ButtonClose:
ExitApp
I added a global variable "showTooltip" to control if the "getFilesInFolder()" displays a tooltip or not. There would probably be a better technique to pass the boolean value to the method but I could not figure how at this time. So I used this global variable as fall back.

I will post the full source of the modified class in the next message.
:thumbup: Author of freeware Quick Access Popup, the powerful Windows folders, apps and documents launcher!
:P Now working on Quick Clipboard Editor
:ugeek: The Automator's Courses on AutoHotkey
User avatar
JnLlnd
Posts: 487
Joined: 29 Sep 2013, 21:29
Location: Montreal, Quebec, Canada
Contact:

Re: Beginners OOP with AHK

17 Dec 2018, 15:46

For convenience, I put the test code and the class code in the same file, test code at the beginning.

FULL TEST AND MODIFIED CLASS CODE

Code: Select all

#SingleInstance force
#Warn All, StdOut 
DetectHiddenWindows, On
SetWorkingDir, %A_ScriptDir%

global showTooltip := True

MyFileSystem := new FileSystem ; create my instance of the class
MyNewContainer := new MyFileSystem.Directory(  ) ; create a container for the specified folder and get files in this folder only
; MyNewContainer := new MyFileSystem.Directory( A_ScriptDir, 3 ) ; use to scan the 2nd and 3rd sub levels of the specified folder
; MyNewContainer := new MyFileSystem.Directory( A_ScriptDir, -1 ) ; use for full scan of the specified folder
; MyNewContainer := new MyFileSystem.Drive( "Z:\", 2 ) ; use to scan the root and 2nd level of the specified drive

if ( showTooltip )
	ToolTip ; hide last tooltip

; get content from the MyNewContainer object
str := MyNewContainer.listFiles() ; get the list of files and folders contained in the MyNewContainer object
; str := MyNewContainer.listFiles("w", 2) ; to get only files and folders with "w" in their name and stop at 3rd level

; show content in Gui
Gui, Add, Edit, w800 r25 ReadOnly, % SubStr( str, 1, 30000 ) . ( StrLen( str ) > 30000 ? "`n..." : "" ) ; limit because of 32k limit of Edit control
Gui, Add, Button, default, Close
GuiControl, Focus, Close
Gui, Show

return

ButtonClose:
ExitApp


; ------------------------------------------------
; Original class FileSystem from nnnik (https://autohotkey.com/boards/viewtopic.php?f=7&t=41332)
; Adapted by JnLlnd (Jean Lalonde)

class FileSystem {
	class FileSystemElement {
		getAttributes() { ;flag string see AutoHotkey Help: FileExist for more infos
			return FileExist( this.name )
		}
		changeAttributes( changeAttributeString ) { ;see FileSetAttrib for more infos
			FileSetAttrib, % changeAttributeString, % this.name
		}
		getPath() {
			return this.name
		}
		getPathName() {
			SplitPath, % this.name, fileName
			return fileName
		}
		getPathDir() { ;same as getDirectory
			return This.getPathDirectory()
		}
		getPathDirectory() {
			SplitPath, % this.name, , fileDirectory
			return fileDirectory
		}
		getPathExtension() {
			SplitPath, % this.name , , , fileExtension
			return fileExtension
		}
		getPathNameNoExtension() {
			SplitPath, % this.name, , , , fileNameNoExtension
			return fileNameNoExtension
		}
		getPathDrive() {
			SplitPath, % this.name, , , , , fileDrive
			return fileDrive
		}
		getTimeAccessed() { ;in YYYYMMDDHH24MISS see AutoHotkey help for more infos
			FileGetTime, timeCreated, % this.name, A
			return timeCreated
		}
		setTimeAccessed( timeAccessed ) {
			FileSetTime, % timeAccessed, % this.name, A
		}
		getTimeModified() {
			FileGetTime, timeModified, % this.name, M
			return timeModified
		}
		setTimeModified( timeModified ) {
			FileSetTime, % timeModified, % this.name, M
		}
		getTimeCreated() {
			FileGetTime, timeCreated, % this.name, C
			return timeCreated
		}
		setTimeCreated( timeCreated ) {
			FileSetTime, % timeCreated, % this.name, C
		}
	}
	class File extends FileSystem.FileSystemElement {
		__New( fileName ) {
			if ( !fileExist( fileName ) )
				Throw exception( "File """ . fileName . """ doesn't exist", "__New", "Exist test returned false" )
			if ( inStr( fileExist( fileName ), "D" ) ) ;if file is a folder or a drive
				Throw exception( "Error creating File", "__New", "Path points to Folder" )
			Loop, Files, % strReplace(fileName,"/","\"), F ;since the fileName refers to a single file this loop will only execute once
				this.name := A_LoopFileLongPath ;and there it will set the path to the value we need
		}
		open( p* ) {
			return FileOpen( this.name, p* )
		}
		getSize( unit := "" ) {
			FileGetSize, fileSize, % this.name, % unit
			return fileSize
		}
		move( newFilePath, overwrite := 0 ) {
			FileMove, % this.name, % newFilePath, % overwrite
		}
		copy( newFilePath, overwrite := 0 ) {
			FileCopy, % this.name, % newFilePath, % overwrite
		}
		delete() {
			FileDelete, % this.name
		}
	}
	class FileContainer extends FileSystem.FileSystemElement {
		__New( path, recurseLevels := 1 ) {
			; path -> folder or drive root to scan
			; recurseLevels -> number of folder levels to scan (default 1 this path only, -1 to scan all, 0 to scan none)
			if ( !fileExist( path ) )
				Throw exception( "Path """ . path . """ doesn't exist", "__New", "Exist test returned false" )
			if ( !inStr( fileExist( path ), "D" ) ) ; if file is not a folder or a drive
				Throw exception( "Error creating File", "__New", "Path does not points to Folder" )
			this.name := path
			if ( SubStr( path, 0, 1 ) = "\" ) ; remove ending backslash for drive roots (like "C:\")
				path := SubStr( path, 1, StrLen( path ) - 1 )
			if !( this.getFilesInFolder( path, recurseLevels ) )
				Throw exception( "Error getting Items", "__New", "Could not get items from container" )
		}
		getFilesInFolder( thisPath, recurseLevels ) {
			; thisPath -> folder or drive root to scan
			; recurseLevels -> number of folder levels to scan including this one
			if ( showToolTip )
				ToolTip, Getting files from:`n%thisPath%
			; if recurseLevels > 0 continue with sub folder
			; if recurseLevels < 0 continue until the end of branch
			; if recurseLevels = 0 stop recursion
			if ( recurseLevels = 0 )
				return true
			this.Items := Object() ; create an object to contain items (files and folders)
			Loop, Files, % thisPath . "\*.*", FD ; do not use "R" here, the class does the recursion below
			{
				if A_LoopFileAttrib contains H,S ; skip hidden or system files
					continue
				if A_LoopFileAttrib contains D ; this is a folder, create Directory object and recurse to sub level
					objItem := new FileSystem.Directory( A_LoopFileFullPath, recurseLevels - 1 ) ; "- 1" to track the number of levels
				else ; this is a file, create File object
					objItem := new FileSystem.File( A_LoopFileFullPath )
				this.addItem(objItem) ; add Directory or File object to Items container
			}
			return true
		}
		listFiles( filter := "", recurseLevels := -1 ) {
			; filter -> exclude items with filter in their name, default empty (include all items)
			; recurseLevels -> number of folder levels to scan (default -1 to scan all, 0 to scan none)
			; if recurseLevels > 0 continue with sub folder
			; if recurseLevels < 0 continue until the end of branch
			; if recurseLevels = 0 stop recursion
			if (recurseLevels = 0)
				return
			thisList := ""
			for intKey, objItem in this.Items {
				if !StrLen(filter) or InStr(objItem.name, filter)
					thisList .= objItem.name . "`n"
				if ( objItem.HasKey( "Items" ) ) ; this is a container, recurse
					thisList .= objItem.listFiles( filter, recurseLevels - 1 ) ; "- 1" to track the number of levels
			}
			return thisList
		}
		addItem(objItem) {
			this.Items.InsertAt(this.Items.Length()+ 1, objItem) ; add Directory or File object to Items container
		}
		getPathName() {
			Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
		}
		getPathExtension() {
			Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
		}
		getPathNameNoExtension() {
			Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
		}
	}
	class Directory extends FileSystem.FileContainer {
	}
	class Drive extends FileSystem.FileContainer {
		changeAttributes( changeAttributeString ) {
			Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
		}
		getPathDir() {
			Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
		}
		getPathDirectory() {
			Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
		}
		getTimeAccessed() {
			Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
		}
		setTimeAccessed( timeStamp ) {
			Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
		}
		getTimeModified() {
			Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
		}
		setTimeModified( timeStamp ) {
			Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
		}
		getTimeCreated() {
			Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
		}
		setTimeCreated( timeStamp ) {
			Throw exception( "Couldn't find method" , A_ThisFunc, "Method: " . A_ThisFunc . " is not available for objects of Class: " . this.__class )
		}
	}
}
:thumbup: Author of freeware Quick Access Popup, the powerful Windows folders, apps and documents launcher!
:P Now working on Quick Clipboard Editor
:ugeek: The Automator's Courses on AutoHotkey
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Beginners OOP with AHK

30 Dec 2018, 07:30

Oh wow - Sory I must have missed your code.
It looks good :thumbup: I kind of expected an array to be honest but a list like that is also incredibly useful.
Recommends AHK Studio
User avatar
JnLlnd
Posts: 487
Joined: 29 Sep 2013, 21:29
Location: Montreal, Quebec, Canada
Contact:

Re: Beginners OOP with AHK

10 Jan 2019, 18:51

nnnik wrote:
30 Dec 2018, 07:30
Oh wow - Sory I must have missed your code.
It looks good :thumbup: I kind of expected an array to be honest but a list like that is also incredibly useful.
Thanks nnik. I took some time before coming back to you as I was doing my first "real" work with classes. One thing I had a hard time to figure out is that a class can be used by creating instances (as for class A in the example below) but also that it can be used without instantiation, just by using its data+method directly (as for class B in the example below). I understand that class A could be "used" many times (A1, A2, ...) and that class B in for "single use".

This was certainly explained in the doc or in your tutorial but I did not "get" it before I faced the following situation. I created a class that reads and process the A_Args object (what it does is not relevant here but I could publish it in the Scripts section). I first created an instance of my class to init the class data and make its methods available. But then, since there is only one A_Args object for a running app, I wondered if instanciating the class was required. So, I changed my code to use the class "as-is" without instanciating it. I just had to create an "Init()" method to prepare the data. This is part of the example B below.

In general, when there are various ways of doing something that are more or less equivalent, I prefer to adopt one method and stick to it. In this case, I would tend to keep the instance approach (A) even when only one instance of the class is needed. Would you recommend to use approach B in this case?

Code: Select all

#SingleInstance force

instanceA := new A("Bob")
B.Init("Joe")

MsgBox, % "1) " . instanceA.name
	. "`n2) " . instanceA.NameUpper()
	. "`n3) " . B.name
	. "`n4) " . B.NameUpper()

return

class A {
	__New(name) {
		this.name := name
	}
	
	NameUpper() {
		StringUpper, upperName, % this.name
		return upperName
	}
}

class B {
	name := ""

	Init(name) {
		B.name := name
	}
	
	NameUpper() {
		StringUpper, upperName, % B.name
		return upperName
	}
}
:thumbup: Author of freeware Quick Access Popup, the powerful Windows folders, apps and documents launcher!
:P Now working on Quick Clipboard Editor
:ugeek: The Automator's Courses on AutoHotkey
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Beginners OOP with AHK

10 Jan 2019, 20:37

Well I would go over this in a later tutorial.
What you discovered here is essentially a design pattern.
Many people seem to find this particular pattern first.

What you have in B would be called a Singleton.
Design Patterns are reoccurring small patterns in our code that we use to tackle specific problems.

In your case you only ever have one use for your class.
So you use a Singleton pattern to represent that.
https://www.autohotkey.com/boards/viewt ... 74&t=38151
^This topic contains some nice ideas for Singletons and other Design Patterns that you might want to use.
Recommends AHK Studio
User avatar
JnLlnd
Posts: 487
Joined: 29 Sep 2013, 21:29
Location: Montreal, Quebec, Canada
Contact:

Re: Beginners OOP with AHK

10 Jan 2019, 22:27

nnnik wrote:
10 Jan 2019, 20:37
In your case you only ever have one use for your class.
So you use a Singleton pattern to represent that.
So my example B should be like this. Classes A and B are now indentical except the 4 "init" lines in B.__New():

Code: Select all

#SingleInstance force

instanceA := new A("Bob")
instanceB := new B("Joe")

MsgBox, % "1) " . instanceA.name
	. "`n2) " . instanceA.NameUpper()
	. "`n3) " . instanceB .name
	. "`n4) " . instanceB .NameUpper()

return

class A {
	__New(name) {
		this.name := name
	}
	
	NameUpper() {
		StringUpper, upperName, % this.name
		return upperName
	}
}

class B {
	__New(name) {
		; singleton design pattern for a single use class
		; (from nnik https://www.autohotkey.com/boards/viewtopic.php?f=74&t=38151#p175344)
		static init ; this is where the instance will be stored
		if init ; this will return true if the class has already been created
			return init ; and it will return this instance rather than creating a new one
		init := This ; this will overwrite the init var with this instance

		this.name := name
	}
	
	NameUpper() {
		StringUpper, upperName, % this.name
		return upperName
	}
}
Thanks for the reference.
:thumbup: Author of freeware Quick Access Popup, the powerful Windows folders, apps and documents launcher!
:P Now working on Quick Clipboard Editor
:ugeek: The Automator's Courses on AutoHotkey
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Beginners OOP with AHK

11 Jan 2019, 03:36

Well there are several types of Singletons in that topic.
I mostly use the super global automatically initiated one
Recommends AHK Studio
User avatar
Hellbent
Posts: 2102
Joined: 23 Sep 2017, 13:34

Re: Beginners OOP with AHK

27 Mar 2019, 11:37

Thank you for sharing this nnnik.
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: Beginners OOP with AHK

27 Mar 2019, 14:13

Thank you for your kind words :)
Glad you liked it
Recommends AHK Studio
User avatar
blackjoker
Posts: 18
Joined: 01 Dec 2020, 02:57

Re: Beginners OOP with AHK

28 Jul 2022, 10:09

Thank you for the topic!
It is still actual and interesting.

Return to “Tutorials (v1)”

Who is online

Users browsing this forum: No registered users and 44 guests