How to create a new window with a certain dimensions (for example 40% of screen size) using Python API?
Functionality-wise similar to File > User Preferences — a new window shows up with User Preferences area type.
How to create a new window with a certain dimensions (for example 40% of screen size) using Python API?
Functionality-wise similar to File > User Preferences — a new window shows up with User Preferences area type.
Currently in Blender 2.79 the only way to manage windows through Python API is by bpy.ops.screen.area_dupli() and bpy.ops.wm.window_duplicate() operators.
But they add new screens to bpy.data.screens, which are not easy to remove.
And there is no way to resize created windows.
UPDATE 1:
In my particular case I can use window created by userpref_show operator, it's size is very close to what I need and it creates 'temp' screen which is deleted after window is closed, so I do not need to cleanup bpy.data.screens afterwards:
import bpy
Call user prefs window
bpy.ops.screen.userpref_show("INVOKE_DEFAULT")
Change area type
area = bpy.context.window_manager.windows[-1].screen.areas[0]
area.type = "TEXT_EDITOR"
UPDATE 2:
With render.view_show operator it is possible to set the exact size of created window by modifying render settings:
import bpy
Modify render settings
render = bpy.context.scene.render
render.resolution_x = 640
render.resolution_y = 480
render.resolution_percentage = 100
Modify preferences (to guaranty new window)
prefs = bpy.context.preferences
prefs.view.render_display_type = "WINDOW"
Call image editor window
bpy.ops.render.view_show("INVOKE_DEFAULT")
Change area type
area = bpy.context.window_manager.windows[-1].screen.areas[0]
area.type = "TEXT_EDITOR"
Restore render settings and preferences
render.resolution_x = original_value
...
I also restore is_dirty tag which affects preferences autosave feature
prefs.is_dirty = original_value
import bpy
# Call user prefs window
bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
# Change area type
area = bpy.context.window_manager.windows[-1].screen.areas[0]
area.type = 'TEXT_EDITOR'
bpy.ops.wm.window_duplicate()
area.type = 'CONSOLE'
– aditia
Oct 30 '18 at 05:45
window_duplicate operator it will add additional screen temp.001 that will not be removed after you close the second window, which will mess up your workflow if you switch between screens as often as I do.
– Mikhail Rachinskiy
Oct 31 '18 at 03:32
I have been struggeling with this as well and would like to share my solution. I wanted to be able to open several windows and change their layout in my script. bpy.ops.screen.userpref_show() opens a window with just a single area but it wouldn't allow me to open several windows and the windows would have the title "Blender User Preferences". bpy.ops.wm.window_duplicate() would allow me to open several windows and make them the same size as the current window, but it would open with my multi-area screen and produce a new screen everytime without removing it. This is my solution:
# Construct context
def context(window=None, screen=None, area=None, region=None):
if window is None: window = C.window
if screen is None: screen = window.screen
if area is None: area = screen.areas[0]
if region is None: region = area.regions[0]
return {'window': window, 'screen': screen, 'area': area, 'region': region}
# Window class wrapper
class Window:
def __init__(self, window):
self.window = window
# Get window screen
def get_screen(self):
return Screen(self.window.screen)
# Create new window
@classmethod
def new(cls, screen_name):
bpy.ops.wm.window_duplicate()
window = cls(C.window_manager.windows[-1])
window.get_screen().set_name(screen_name).join_all()
return window
# Screen class wrapper
class Screen:
def __init__(self, screen):
self.screen = screen
# Get name
def get_name(self, name):
return self.screen.name
# Set name
def set_name(self, name):
self.screen.name = name
# Return self for method chaining
return self
# Get scene
def get_scene(self):
return self.screen.scene
# Set scene
def set_scene(self, scene):
self.screen.scene = scene
# Return self for method chaining
return self
# Get area
def area_at(self, index):
return Area(self.screen.areas[index])
# Split area and return new one
def split_area(self, area, dir, factor):
# Set direction
if 'vertical'.startswith(dir.lower()): dir = 'VERTICAL'
elif 'horizontal'.startswith(dir.lower()): dir = 'HORIZONTAL'
else: raise ValueError("Invalid direction \"%s\""%dir)
bpy.ops.screen.area_split(
context(screen=self.screen, area=area.area),
direction=dir, factor=factor
)
return Area(self.screen.areas[-1])
# Join areas and return success status
def join_areas(self, area1, area2):
# Try joining areas
result = bpy.ops.screen.area_join(context(screen=self.screen),
min_x=area1.x, min_y=area1.y,
max_x=area2.x, max_y=area2.y
)
# Return eliminated area
return ('FINISHED' in result)
# Join all areas
def join_all(self):
# Create area index
index = self.AreaIndex(self.screen.areas)
# Join all areas
for i in range(index.count):
for key in index.x:
for a1, a2 in index.x_pair_iterator(key):
if self.join_areas(a1, a2):
index.remove_area(a2)
break
for key in index.y:
for a1, a2 in index.y_pair_iterator(key):
if self.join_areas(a1, a2):
index.remove_area(a2)
break
if index.count <= 1: break
# Return self for method chaining
return self
# Area index for area joining
class AreaIndex:
# Create area index
def __init__(self, areas):
# Create index maps
self.x = {}
self.y = {}
self.count = len(areas)
# Insert areas into indices
for area in areas:
if not area.x in self.x: self.x[area.x] = []
self.x[area.x].append(area)
if not area.y in self.y: self.y[area.y] = []
self.y[area.y].append(area)
# Sort area lists
for key in self.x: self.x[key].sort(key=lambda a: a.y)
for key in self.y: self.y[key].sort(key=lambda a: a.x)
# Remove area from index
def remove_area(self, area):
self.x[area.x].remove(area)
self.y[area.y].remove(area)
self.count -= 1
# Iterate over area pairs for x key
def x_pair_iterator(self, key):
for i in range(len(self.x[key]) - 1):
yield (self.x[key][i], self.x[key][i + 1])
# Iterate over area pairs for x key
def y_pair_iterator(self, key):
for i in range(len(self.y[key]) - 1):
yield (self.y[key][i], self.y[key][i + 1])
# Area class wrapper
class Area:
def __init__(self, area):
self.area = area
# Get area type
def get_type(self):
return self.area.type
# Set area type
def set_type(self, type):
self.area.type = type
# Return self for method chaining
return self
SCREEN_NAME = "temp"
# Create new window
window = Window.new(SCREEN_NAME)
screen = window.get_screen().set_scene(D.scenes['Test'])
# Setup areas
node_area = screen.area_at(0).set_type('NODE_EDITOR')
movie_area = screen.split_area(node_area, 'v', 0.7).set_type('CLIP_EDITOR')
# Get used screens
used_screens = set(
w.screen for w in C.window_manager.windows
if w.screen.name.startswith(SCREEN_NAME)
)
# Delete unused screens
screen_backup = C.window.screen
for s in D.screens:
if s.name.startswith(SCREEN_NAME) and s not in used_screens:
bpy.ops.screen.delete(context(screen=s))
C.window.screen = screen_backup